From 58956fa37ab75f02618544f012732cd9d72c186d Mon Sep 17 00:00:00 2001 From: dotfiles-core sync bot <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 21:55:30 +0000 Subject: [PATCH 1/2] Squashed 'core/' changes from 3ace724..e9cf907 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit e9cf907 Merge pull request #241 from dotgibson/release/v3.7.0 87f0f56 release v3.7.0 c610e78 Merge pull request #240 from dotgibson/feat/starship-nvim-fleet-fixes 15f1155 feat(nvim,starship): cross-fleet fixes — Windows DAP/clipboard, venv/package prompt, robustness 28c168e Merge pull request #239 from dotgibson/core-triage-g4 9345e05 fix(ci): grant release-readiness pull-requests: read for gh pr list 64d05de fix(ci): stop routines from running nvim plugin build hooks with the token present d0b790d refactor(zsh,tmux): drop hot-path forks, redundant logic, and brittle help (#237) 202d9d0 fix(security): verify hook tool downloads and least-privilege the routine tools (#235) 14ccb08 Merge pull request #236 from dotgibson/core-triage-g2 31cad5e fix(nvim,zsh): un-deprecate cheatsheet highlight API and harden history hygiene git-subtree-dir: core git-subtree-split: e9cf907f5849dfae2f3313d840fcbafb3772b83a --- .claude/commands/drift-triage.md | 2 +- .claude/commands/freshness-triage.md | 12 +++-- .claude/commands/release-readiness.md | 8 +-- .claude/hooks/session-start.sh | 48 ++++++++++++----- .github/workflows/claude-routines.yml | 26 ++++++--- CHANGELOG.md | 65 ++++++++++++++++++++++ core.version | 2 +- nvim/lua/gerrrt/cheatsheet.lua | 7 +-- nvim/lua/gerrrt/config/clipboard.lua | 33 +++++++++++- nvim/lua/gerrrt/plugins/blink-cmp.lua | 4 +- nvim/lua/gerrrt/plugins/nvim-dap-ui.lua | 15 ++++-- nvim/lua/gerrrt/servers/emmet_ls.lua | 1 + nvim/lua/gerrrt/servers/init.lua | 9 +++- scripts/freshness-dashboard.sh | 13 ++++- scripts/test-core.sh | 2 + scripts/update-nvim-plugins.sh | 9 +++- scripts/update-plugins.sh | 9 +++- starship/starship.toml | 43 +++++++++++++-- tmux/tmux.conf | 4 +- zsh/functions.zsh | 13 ++--- zsh/git.zsh | 23 +++++--- zsh/history.zsh | 8 ++- zsh/ui.zsh | 72 ++++++++++++++----------- 23 files changed, 331 insertions(+), 97 deletions(-) diff --git a/.claude/commands/drift-triage.md b/.claude/commands/drift-triage.md index f60b183..5de2277 100644 --- a/.claude/commands/drift-triage.md +++ b/.claude/commands/drift-triage.md @@ -1,7 +1,7 @@ --- description: Interpret fleet-drift into ranked, per-repo remediation (report-first) argument-hint: "[repo, optional — defaults to the whole fleet]" -allowed-tools: Read, Grep, Glob, Bash, WebSearch +allowed-tools: Read, Grep, Glob, WebSearch, Bash(./scripts/fleet-drift.sh:*), Bash(git log:*), Bash(git describe:*), Bash(git tag:*) --- # /drift-triage diff --git a/.claude/commands/freshness-triage.md b/.claude/commands/freshness-triage.md index fa58047..bfdfcc6 100644 --- a/.claude/commands/freshness-triage.md +++ b/.claude/commands/freshness-triage.md @@ -1,7 +1,7 @@ --- description: Review open dependency-bump PRs against upstream changelogs argument-hint: "[PR number, optional — defaults to all open bot PRs]" -allowed-tools: Task, Read, Grep, Glob, WebSearch, WebFetch, Bash(./scripts/update-plugins.sh --check), Bash(./scripts/update-nvim-plugins.sh --check), Bash(git log:*), Bash(git diff:*) +allowed-tools: Task, Read, Grep, Glob, WebSearch, WebFetch, Bash(./scripts/update-plugins.sh --check), Bash(git log:*), Bash(git diff:*), Bash(gh pr list:*), Bash(gh pr view:*), Bash(gh pr diff:*), Bash(gh pr checks:*) --- # /freshness-triage @@ -19,13 +19,19 @@ Target for this run: **$ARGUMENTS** (empty = all open automation PRs). `automation/freshness-zsh-plugins` and `automation/freshness-nvim-plugins`. - **`dependabot.yml`** (weekly) — bumps GitHub Actions in `.github/workflows/`. -The `--check` modes are the source of truth for "is it behind": +For the zsh pins, `--check` is the source of truth for "is it behind" and is safe to run +(it only `git ls-remote`s + `zsh -n` parses — no upstream code runs): ```bash ./scripts/update-plugins.sh --check -./scripts/update-nvim-plugins.sh --check ``` +For the **nvim** pins, do NOT run `update-nvim-plugins.sh --check` here — it runs +`nvim --headless +Lazy! sync`, which executes upstream plugin _build hooks_ inside this +token-bearing job (a supply-chain path to `CLAUDE_CODE_OAUTH_TOKEN`). Read the staleness +from the open `automation/freshness-nvim-plugins` PR the bot already opened — `gh pr diff` +/ `gh pr view` show exactly which pins moved. + ## What to do per PR 1. **Identify what moved** — read the diff: which plugin/action, from which pin to diff --git a/.claude/commands/release-readiness.md b/.claude/commands/release-readiness.md index 61ee2dc..48f5f87 100644 --- a/.claude/commands/release-readiness.md +++ b/.claude/commands/release-readiness.md @@ -1,7 +1,7 @@ --- description: Go/no-go readiness check before cutting a Core release argument-hint: "[target version X.Y.Z — optional]" -allowed-tools: Task, Read, Grep, Glob, Bash(./scripts/audit-core.sh:*), Bash(./scripts/fleet-drift.sh:*), Bash(./scripts/update-plugins.sh --check), Bash(./scripts/update-nvim-plugins.sh --check), Bash(git log:*), Bash(git tag:*), Bash(cat core.version) +allowed-tools: Task, Read, Grep, Glob, Bash(./scripts/audit-core.sh:*), Bash(./scripts/fleet-drift.sh:*), Bash(./scripts/update-plugins.sh --check), Bash(git log:*), Bash(git tag:*), Bash(cat core.version), Bash(gh pr list:*), Bash(gh issue list:*), Bash(gh run list:*) --- # /release-readiness @@ -24,8 +24,10 @@ Target for this run: **$ARGUMENTS** (empty = infer the next version from the unr fresh one). Propose the next SemVer from the unreleased content: a breaking change → major, a `feat` → minor, only `fix`/`chore`/`docs` → patch. 4. **Is the fleet in a releasable state?** `fleet-drift.sh` (are the OS repos on the latest - Core?) and pin freshness (`update-*-plugins.sh --check`). A release fans out, so surface - any drift or stale pins that ought to settle first — **advisory**, not hard blockers. + Core?) and pin freshness (`update-plugins.sh --check` for zsh; for nvim, note any open + `automation/freshness-nvim-plugins` PR via `gh pr list` rather than rebuilding locally, + which would run upstream build hooks in this token-bearing job). A release fans out, so + surface any drift or stale pins that ought to settle first — **advisory**, not hard blockers. 5. **Any open blockers?** Open `freshness-triage` **Hold** verdicts, a failing scheduled sweep, or a security bump that should ride the release. diff --git a/.claude/hooks/session-start.sh b/.claude/hooks/session-start.sh index e230eb7..d9c93fa 100755 --- a/.claude/hooks/session-start.sh +++ b/.claude/hooks/session-start.sh @@ -73,12 +73,22 @@ CA=/etc/ssl/certs/ca-certificates.crt [ -r "$CA" ] && export NODE_EXTRA_CA_CERTS="$CA" # Helper: install a pinned release tarball to /usr/local/bin (like CI does). -install_tarball() { # install_tarball [member] - local bin="$1" url="$2" flags="$3" member="${4:-}" +# Downloads to a file, VERIFIES the pinned SHA-256 (fail closed — no checksum, no +# install), THEN extracts. Mirrors setup-core-tools/action.yml: the tarball must +# never be piped straight into tar unverified (a MITM'd asset would run as the gate). +install_tarball() { # install_tarball [member] + local bin="$1" url="$2" sha="$3" flags="$4" member="${5:-}" have "$bin" && return 0 - local tmp + local tmp f tmp="$(mktemp -d)" - if curl -fsSL "$url" | tar "$flags" -C "$tmp" ${member:+"$member"} 2>/dev/null; then + f="$tmp/${url##*/}" + if [ -z "$sha" ]; then + log "no pinned SHA-256 for $bin — refusing unverified install (check scripts/tool-versions.env)" + elif ! curl -fsSL -o "$f" "$url"; then + log "could not download $bin from $url" + elif ! echo "${sha} ${f}" | sha256sum -c - >/dev/null 2>&1; then + log "SHA-256 mismatch for $bin — refusing to install (expected ${sha})" + elif tar "$flags" -f "$f" -C "$tmp" ${member:+"$member"} 2>/dev/null; then local found found="$(find "$tmp" -type f -name "$bin" 2>/dev/null | head -n1)" [ -n "$found" ] && $SUDO install -m755 "$found" "/usr/local/bin/$bin" 2>/dev/null @@ -87,25 +97,35 @@ install_tarball() { # install_tarball [member] have "$bin" || log "could not install $bin from $url" } -# ── shellcheck (pinned) ─────────────────────────────────────────────────────── +# ── shellcheck (pinned + verified) ──────────────────────────────────────────── [ -n "${SHELLCHECK_VERSION:-}" ] && install_tarball shellcheck \ - "https://github.com/koalaman/shellcheck/releases/download/v${SHELLCHECK_VERSION}/shellcheck-v${SHELLCHECK_VERSION}.linux.x86_64.tar.xz" -xJ + "https://github.com/koalaman/shellcheck/releases/download/v${SHELLCHECK_VERSION}/shellcheck-v${SHELLCHECK_VERSION}.linux.x86_64.tar.xz" "${SHELLCHECK_SHA256:-}" -xJ -# ── actionlint (pinned) ─────────────────────────────────────────────────────── +# ── actionlint (pinned + verified) ──────────────────────────────────────────── [ -n "${ACTIONLINT_VERSION:-}" ] && install_tarball actionlint \ - "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz" -xz actionlint + "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz" "${ACTIONLINT_SHA256:-}" -xz actionlint -# ── gitleaks (pinned) — the audit's secrets section runs `gitleaks dir`, so without -# this a remote session's secrets gate would SKIP: a "green because absent" for secrets, -# the exact false-green this hook exists to prevent. Same tarball pattern as above. ── +# ── gitleaks (pinned + verified) — the audit's secrets section runs `gitleaks dir`, so +# without this a remote session's secrets gate would SKIP: a "green because absent" for +# secrets, the exact false-green this hook exists to prevent. Same tarball pattern above. ── [ -n "${GITLEAKS_VERSION:-}" ] && install_tarball gitleaks \ - "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" -xz gitleaks + "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" "${GITLEAKS_SHA256:-}" -xz gitleaks -# ── neovim (pinned) — extracted to /opt, symlinked onto PATH (CI's Linux path) ─ +# ── neovim (pinned + verified) — extracted to /opt, symlinked onto PATH (CI's Linux path). +# Verify the pinned SHA-256 before unpacking as root — same fail-closed rule as above. ── if [ -n "${NVIM_VERSION:-}" ] && ! have nvim; then - if curl -fsSL "https://github.com/neovim/neovim/releases/download/v${NVIM_VERSION}/nvim-linux-x86_64.tar.gz" | $SUDO tar -xz -C /opt 2>/dev/null; then + nvim_tmp="$(mktemp -d)" + nvim_tgz="$nvim_tmp/nvim-linux-x86_64.tar.gz" + if [ -z "${NVIM_SHA256:-}" ]; then + log "no pinned SHA-256 for neovim — refusing unverified install (check scripts/tool-versions.env)" + elif ! curl -fsSL -o "$nvim_tgz" "https://github.com/neovim/neovim/releases/download/v${NVIM_VERSION}/nvim-linux-x86_64.tar.gz"; then + log "could not download neovim" + elif ! echo "${NVIM_SHA256} ${nvim_tgz}" | sha256sum -c - >/dev/null 2>&1; then + log "SHA-256 mismatch for neovim — refusing to install (expected ${NVIM_SHA256})" + elif $SUDO tar -xz -f "$nvim_tgz" -C /opt 2>/dev/null; then $SUDO ln -sf /opt/nvim-linux-x86_64/bin/nvim /usr/local/bin/nvim 2>/dev/null fi + rm -rf "$nvim_tmp" have nvim || log "could not install neovim" fi diff --git a/.github/workflows/claude-routines.yml b/.github/workflows/claude-routines.yml index 6bc9a80..a4f1b26 100644 --- a/.github/workflows/claude-routines.yml +++ b/.github/workflows/claude-routines.yml @@ -14,6 +14,17 @@ name: claude-routines # INERT BY DEFAULT: with no CLAUDE_CODE_OAUTH_TOKEN secret the jobs no-op with a warning (so a # fork — or this repo before you opt in — never gets a red X). Add the secret to enable. # Scheduled runs draw on the subscription's usage; the weekly cadence keeps that predictable. +# +# LEAST PRIVILEGE: each job's `--allowedTools` MIRRORS the routine's own `allowed-tools` +# frontmatter (.claude/commands/.md) and is never broader. Critically, a job that +# reads web content (WebFetch/WebSearch) must NOT also grant unrestricted `Bash` — a prompt +# injection in fetched content could otherwise run arbitrary shell and exfiltrate +# CLAUDE_CODE_OAUTH_TOKEN. Scope Bash to the exact verbs the routine runs (see shell-review). +# For the same reason, freshness-triage / release-readiness are NOT granted +# `update-nvim-plugins.sh --check`: that script runs `nvim --headless +Lazy! sync`, which +# executes UPSTREAM plugin build hooks in this token-bearing job (a supply-chain path to the +# token). They read nvim-pin staleness from the open freshness PR via `gh` instead. The zsh +# `update-plugins.sh --check` stays — it only `git ls-remote`s + `zsh -n` parses, no code exec. on: schedule: @@ -90,7 +101,7 @@ jobs: claude -p "/tool-scout" \ --model opus \ --permission-mode default \ - --allowedTools "Task,Read,Grep,Glob,Bash,WebSearch,WebFetch" \ + --allowedTools "Task,Read,Grep,Glob,WebSearch,WebFetch" \ --output-format text | tee "$RUNNER_TEMP/tool-scout.md" - name: File the report as an issue run: bash .github/workflows/file-routine-issue.sh "$ISSUE_TITLE" "$RUNNER_TEMP/tool-scout.md" @@ -139,7 +150,7 @@ jobs: --add-dir "$GITHUB_WORKSPACE" \ --model opus \ --permission-mode default \ - --allowedTools "Task,Read,Grep,Glob,Bash" \ + --allowedTools "Task,Read,Grep,Glob,Bash(git status:*),Bash(git diff:*),Bash(git ls-files:*),Bash(git log:*),Bash(ls:*)" \ --output-format text | tee "$RUNNER_TEMP/doc-audit.md" - name: File the report as an issue working-directory: dotfiles-core @@ -177,7 +188,7 @@ jobs: claude -p "/freshness-triage" \ --model opus \ --permission-mode default \ - --allowedTools "Task,Read,Grep,Glob,Bash,WebSearch,WebFetch" \ + --allowedTools "Task,Read,Grep,Glob,WebSearch,WebFetch,Bash(./scripts/update-plugins.sh --check),Bash(git log:*),Bash(git diff:*),Bash(gh pr list:*),Bash(gh pr view:*),Bash(gh pr diff:*),Bash(gh pr checks:*)" \ --output-format text | tee "$RUNNER_TEMP/freshness-triage.md" - name: File the report as an issue run: bash .github/workflows/file-routine-issue.sh "$ISSUE_TITLE" "$RUNNER_TEMP/freshness-triage.md" @@ -214,7 +225,7 @@ jobs: claude -p "/modernize" \ --model opus \ --permission-mode default \ - --allowedTools "Task,Read,Grep,Glob,Bash,WebSearch,WebFetch" \ + --allowedTools "Task,Read,Grep,Glob,WebSearch,WebFetch,Bash(./scripts/check-modern.sh),Bash(git ls-files:*),Bash(git log:*)" \ --output-format text | tee "$RUNNER_TEMP/modernize.md" - name: File the report as an issue run: bash .github/workflows/file-routine-issue.sh "$ISSUE_TITLE" "$RUNNER_TEMP/modernize.md" @@ -235,6 +246,7 @@ jobs: permissions: contents: read issues: write + pull-requests: read # gh pr list — observe the open freshness PR + Hold verdicts env: CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} GH_TOKEN: ${{ github.token }} @@ -252,7 +264,7 @@ jobs: claude -p "/release-readiness" \ --model opus \ --permission-mode default \ - --allowedTools "Task,Read,Grep,Glob,Bash,WebSearch,WebFetch" \ + --allowedTools "Task,Read,Grep,Glob,Bash(./scripts/audit-core.sh:*),Bash(./scripts/fleet-drift.sh:*),Bash(./scripts/update-plugins.sh --check),Bash(git log:*),Bash(git tag:*),Bash(cat core.version),Bash(gh pr list:*),Bash(gh issue list:*),Bash(gh run list:*)" \ --output-format text | tee "$RUNNER_TEMP/release-readiness.md" - name: File the report as an issue run: bash .github/workflows/file-routine-issue.sh "$ISSUE_TITLE" "$RUNNER_TEMP/release-readiness.md" @@ -287,7 +299,7 @@ jobs: claude -p "/release-notes" \ --model opus \ --permission-mode default \ - --allowedTools "Task,Read,Grep,Glob,Bash" \ + --allowedTools "Task,Read,Grep,Glob,Bash(make release-notes),Bash(./scripts/gen-release-notes.sh:*),Bash(git log:*),Bash(git tag:*)" \ --output-format text | tee "$RUNNER_TEMP/release-notes.md" - name: File the report as an issue run: bash .github/workflows/file-routine-issue.sh "$ISSUE_TITLE" "$RUNNER_TEMP/release-notes.md" @@ -372,7 +384,7 @@ jobs: --add-dir "$GITHUB_WORKSPACE" \ --model opus \ --permission-mode default \ - --allowedTools "Read,Grep,Glob,Bash,WebSearch" \ + --allowedTools "Read,Grep,Glob,WebSearch,Bash(./scripts/fleet-drift.sh:*),Bash(git log:*),Bash(git describe:*),Bash(git tag:*)" \ --output-format text | tee "$RUNNER_TEMP/drift-triage.md" - name: File the report as an issue working-directory: dotfiles-core diff --git a/CHANGELOG.md b/CHANGELOG.md index 165534e..fe6dce3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,71 @@ commit (`git tag -a vX.Y.Z -m vX.Y.Z`). ## [Unreleased] +## [v3.7.0] - 2026-07-17 + +### Changed + +- **Neovim clipboard gains a gated OSC52 last-resort provider.** When no native clipboard + backend is on PATH (no Core `clip`/`clip-paste`, no `clip.exe`, and none of `pbcopy` / + `wl-copy` / `xclip` / `xsel` / `win32yank`), `"+y`/`"+p` route over the terminal's OSC52 + sequence — closing the "yank does nothing over tmux/psmux/SSH" gap on headless/remote boxes. + A working native provider is never overridden, so a normal desktop is unaffected. +- **Neovim clipboard paste uses `Get-Clipboard -Raw` on Windows**, so multi-line pastes no + longer arrive split/CRLF-mangled. +- **Windows-correct Neovim Python DAP + LuaSnip build.** debugpy/venv interpreters resolve to + `Scripts\python.exe` (and `python`, not `python3`) on Windows; LuaSnip skips its + `make install_jsregexp` build where there's no toolchain. All gated on `has("win32")`. +- **Neovim LSP capabilities fetch is `pcall`-guarded** (a blink load failure no longer aborts + the whole server stack), and `emmet_ls` now attaches to `html`. +- **Starship prompt gains venv, package, git-metrics, and WSL indicators.** `[python]` shows the + active `$virtualenv`; a `$package` version indicator and an `$env_var` slot are wired into + `format` (surfacing a previously-unplaced `ENGAGEMENT` var and a new `WSL_DISTRO_NAME` badge); + `git_metrics` is enabled; a documented opt-in `docker_version` custom is included (off by default). +- **`git_main_branch` resolves the trunk in one call.** It reads `origin/HEAD` directly + (`git symbolic-ref`) and only falls back to probing the trunk-name candidate list when + that is unset — instead of firing up to 18 `git show-ref` subprocesses every call. It sits + on the hot path (`gcom` / `gswm` / `grbm`). +- **`git_current_branch` uses the git porcelain.** It now reads `git branch --show-current` + (git 2.22+) instead of the hand-rolled `symbolic-ref` + return-code dance; the short-SHA + fallback on a detached HEAD and the empty result outside a repo are both preserved. +- **`_core_suggest` no longer forks per candidate.** `_core_lev` gained a fork-free out-var + mode, so scoring a mistyped Core verb against the alias/subcommand list runs in-process + rather than spawning a command substitution for each candidate (~80+ on a bad `please`). +- **`pullall` tallies its summary in a single `awk` pass** instead of four `grep -c` scans of + the same buffer. +- **Collapsed a dead `status-left` conditional** in `tmux/tmux.conf` to a constant — all three + branches resolved to the same colour. +- **Decoupled `--help` from header line numbers.** `update-plugins.sh`, + `update-nvim-plugins.sh`, and `freshness-dashboard.sh` now print usage from a heredoc rather + than `sed -n ',p' "$0"`, which silently mis-printed whenever the header comment moved + (the coupling `sync-core.sh` already documents having removed). +- **Removed redundant zsh history setopts** (`zsh/history.zsh`): `INC_APPEND_HISTORY` (implied + by `SHARE_HISTORY`) and `HIST_IGNORE_DUPS` (superseded by `HIST_IGNORE_ALL_DUPS`) were + no-ops, so they are dropped. + +### Security + +- **session-start hook verifies its tool downloads.** `install_tarball` and the neovim + install in `.claude/hooks/session-start.sh` now download to a file and check the pinned + SHA-256 from `scripts/tool-versions.env` before extracting — failing closed when a + checksum is absent or mismatched — instead of piping `curl … | tar` unverified. Brings the + remote-session gate toolchain in line with the CI composite action, which already verifies. +- **claude-routines run with least-privilege tools.** Each job's `--allowedTools` now mirrors + its routine's own `allowed-tools` frontmatter rather than granting unrestricted `Bash`; the + web-reading routines (tool-scout, freshness-triage, modernize, release-readiness, + drift-triage) no longer pair arbitrary shell with `WebFetch`/`WebSearch`, closing a + prompt-injection → `CLAUDE_CODE_OAUTH_TOKEN` exfiltration path. `drift-triage`'s command + frontmatter is scoped to match. +- **`HISTORY_IGNORE` covers more secret shapes** (`zsh/history.zsh`): `--flag=value` forms and + env-assignment credentials (`TOKEN=`, `PASSWORD=`, `*ACCESS_KEY=`, `APIKEY`) that the + space-only patterns let slip now stay out of the plaintext `$HISTFILE`. + +### Fixed + +- **Cheatsheet uses a non-deprecated highlight API.** `nvim/lua/gerrrt/cheatsheet.lua` now + calls `nvim_buf_set_extmark` instead of the deprecated `nvim_buf_add_highlight` (slated for + removal on Neovim nightly), so `:Cheatsheet` / `?` keeps working on current Neovim. + ## [v3.6.1] - 2026-07-16 ### Documentation diff --git a/core.version b/core.version index 9575d51..7c69a55 100644 --- a/core.version +++ b/core.version @@ -1 +1 @@ -3.6.1 +3.7.0 diff --git a/nvim/lua/gerrrt/cheatsheet.lua b/nvim/lua/gerrrt/cheatsheet.lua index ca087ed..5704421 100644 --- a/nvim/lua/gerrrt/cheatsheet.lua +++ b/nvim/lua/gerrrt/cheatsheet.lua @@ -94,7 +94,7 @@ M.sections = { { "fh", "Help tags" }, { "fk", "Keymaps" }, { "ft", "Todo comments" }, - { "fd / fD", "Diagnostics doc / workspace" }, + { "fx / fX", "Diagnostics doc / workspace" }, { "fs / fw", "Symbols doc / workspace" }, }, { @@ -312,7 +312,7 @@ local function pack(cards, ncol) end -- Merge the packed columns into flat buffer lines + a flat highlight list (0-indexed rows, byte --- cols) ready for nvim_buf_set_lines / nvim_buf_add_highlight. +-- cols) ready for nvim_buf_set_lines / nvim_buf_set_extmark. local function compose(cols, height) local ncol = #cols local out_lines, out_hls = {}, {} @@ -369,7 +369,8 @@ function M.open() vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines) local ns = vim.api.nvim_create_namespace("gerrrt_cheatsheet") for _, h in ipairs(hls) do - vim.api.nvim_buf_add_highlight(buf, ns, h.hl, h.row, h.s, h.e) + -- nvim_buf_add_highlight is deprecated (0.11+); set_extmark is the exact equivalent + vim.api.nvim_buf_set_extmark(buf, ns, h.row, h.s, { end_col = h.e, hl_group = h.hl }) end vim.bo[buf].modifiable = false vim.bo[buf].filetype = "cheatsheet" diff --git a/nvim/lua/gerrrt/config/clipboard.lua b/nvim/lua/gerrrt/config/clipboard.lua index 60e98a8..9de0288 100644 --- a/nvim/lua/gerrrt/config/clipboard.lua +++ b/nvim/lua/gerrrt/config/clipboard.lua @@ -47,8 +47,37 @@ elseif vim.fn.executable("clip.exe") == 1 then ["*"] = "clip.exe", }, paste = { - ["+"] = { pwsh, "-NoProfile", "-Command", "Get-Clipboard" }, - ["*"] = { pwsh, "-NoProfile", "-Command", "Get-Clipboard" }, + -- -Raw returns the clipboard as a single string; bare Get-Clipboard yields a + -- string[] split on newlines, which nvim rejoins with CRLF and mangles multi-line pastes. + ["+"] = { pwsh, "-NoProfile", "-Command", "Get-Clipboard -Raw" }, + ["*"] = { pwsh, "-NoProfile", "-Command", "Get-Clipboard -Raw" }, + }, + cache_enabled = 0, + } +elseif vim.fn.has("nvim-0.10") == 1 + and vim.fn.executable("pbcopy") == 0 + and vim.fn.executable("wl-copy") == 0 + and vim.fn.executable("xclip") == 0 + and vim.fn.executable("xsel") == 0 + and vim.fn.executable("win32yank") == 0 +then + -- Last-resort fallback, applied ONLY when there is genuinely no native clipboard backend + -- on PATH: no Core clip/clip.exe AND none of the OS-native tools Neovim would auto-detect + -- (pbcopy / wl-copy / xclip / xsel / win32yank). This keeps the module contract above — a + -- working native provider is NEVER overridden (so its blocking OSC52 paste query is never + -- imposed on a box that has a real provider) — while still closing the "yank does nothing + -- over tmux/psmux/SSH" gap on a headless/remote box. Neovim 0.10+'s OSC52 provider carries + -- yanks to the system clipboard over the terminal escape sequence, no external helper needed. + local osc52 = require("vim.ui.clipboard.osc52") + vim.g.clipboard = { + name = "clip-osc52", + copy = { + ["+"] = osc52.copy("+"), + ["*"] = osc52.copy("+"), + }, + paste = { + ["+"] = osc52.paste("+"), + ["*"] = osc52.paste("+"), }, cache_enabled = 0, } diff --git a/nvim/lua/gerrrt/plugins/blink-cmp.lua b/nvim/lua/gerrrt/plugins/blink-cmp.lua index 00ca9b6..0c252a5 100644 --- a/nvim/lua/gerrrt/plugins/blink-cmp.lua +++ b/nvim/lua/gerrrt/plugins/blink-cmp.lua @@ -18,7 +18,9 @@ return { event = "InsertEnter", dependencies = { "rafamadriz/friendly-snippets", - { "L3MON4D3/LuaSnip", version = "v2.*", build = "make install_jsregexp" }, + -- install_jsregexp needs make + a C toolchain, absent on stock Windows; skip the build there. + -- LuaSnip degrades gracefully without jsregexp (only regex-transform snippets are affected). + { "L3MON4D3/LuaSnip", version = "v2.*", build = vim.fn.has("win32") == 0 and "make install_jsregexp" or nil }, }, ---@module 'blink.cmp' ---@type blink.cmp.Config diff --git a/nvim/lua/gerrrt/plugins/nvim-dap-ui.lua b/nvim/lua/gerrrt/plugins/nvim-dap-ui.lua index 9cde344..4d73e71 100644 --- a/nvim/lua/gerrrt/plugins/nvim-dap-ui.lua +++ b/nvim/lua/gerrrt/plugins/nvim-dap-ui.lua @@ -120,19 +120,24 @@ return { }) -- ── uv-aware Python debugging ──────────────────────────────────────────── + -- Windows venvs put the interpreter at \Scripts\python.exe (not /bin/python) and ship + -- only `python` on PATH (no `python3`), so the interpreter paths below branch on OS. + local is_win = vim.fn.has("win32") == 1 + local venv_python = is_win and "\\Scripts\\python.exe" or "/bin/python" local function uv_python() if vim.env.VIRTUAL_ENV and vim.env.VIRTUAL_ENV ~= "" then - return vim.env.VIRTUAL_ENV .. "/bin/python" + return vim.env.VIRTUAL_ENV .. venv_python end - local venv = vim.fn.getcwd() .. "/.venv/bin/python" + local venv = vim.fn.getcwd() .. (is_win and "/.venv\\Scripts\\python.exe" or "/.venv/bin/python") if vim.fn.executable(venv) == 1 then return venv end - local sys = vim.fn.exepath("python3") + local sys = vim.fn.exepath(is_win and "python" or "python3") return sys ~= "" and sys or "python" end - local debugpy_python = vim.fn.stdpath("data") .. "/mason/packages/debugpy/venv/bin/python" + local debugpy_python = vim.fn.stdpath("data") + .. (is_win and "/mason/packages/debugpy/venv\\Scripts\\python.exe" or "/mason/packages/debugpy/venv/bin/python") dap.adapters.python = function(cb, config) if config.request == "attach" then @@ -141,7 +146,7 @@ return { else cb({ type = "executable", - command = vim.fn.executable(debugpy_python) == 1 and debugpy_python or "python3", + command = vim.fn.executable(debugpy_python) == 1 and debugpy_python or (is_win and "python" or "python3"), args = { "-m", "debugpy.adapter" }, options = { source_filetype = "python" }, }) diff --git a/nvim/lua/gerrrt/servers/emmet_ls.lua b/nvim/lua/gerrrt/servers/emmet_ls.lua index 58c1b65..e9f506d 100644 --- a/nvim/lua/gerrrt/servers/emmet_ls.lua +++ b/nvim/lua/gerrrt/servers/emmet_ls.lua @@ -2,6 +2,7 @@ return function(capabilities) vim.lsp.config("emmet_ls", { capabilities = capabilities, filetypes = { + "html", "typescript", "javascript", "javascriptreact", diff --git a/nvim/lua/gerrrt/servers/init.lua b/nvim/lua/gerrrt/servers/init.lua index 5276854..770c289 100644 --- a/nvim/lua/gerrrt/servers/init.lua +++ b/nvim/lua/gerrrt/servers/init.lua @@ -1,7 +1,14 @@ -- LSP client capabilities now come from blink.cmp (was: cmp_nvim_lsp). blink -- advertises the completion capabilities its sources support; get_lsp_capabilities -- merges them onto Neovim's defaults. (Migration: see plugins/blink-cmp.lua.) -local capabilities = require("blink.cmp").get_lsp_capabilities() +-- Guard the blink require: if it fails to load (fresh box, plugin not built yet), fall back to +-- Neovim's default client capabilities rather than aborting the whole server stack. +local ok, capabilities = pcall(function() + return require("blink.cmp").get_lsp_capabilities() +end) +if not ok then + capabilities = vim.lsp.protocol.make_client_capabilities() +end -- Language Server Protocol (LSP) require("gerrrt.servers.lua_ls")(capabilities) diff --git a/scripts/freshness-dashboard.sh b/scripts/freshness-dashboard.sh index 40297b6..4928f07 100755 --- a/scripts/freshness-dashboard.sh +++ b/scripts/freshness-dashboard.sh @@ -39,7 +39,18 @@ OWNER="${GITHUB_REPOSITORY_OWNER:-dotgibson}" while [ $# -gt 0 ]; do case "$1" in --root) ROOT="${2:?--root needs a directory}"; shift 2 ;; - -h | --help) sed -n '2,19p' "$0"; exit 0 ;; + -h | --help) + cat <<'EOF' +freshness-dashboard.sh — compose the weekly fleet freshness dashboard (markdown → stdout). + + ./scripts/freshness-dashboard.sh build the dashboard for the whole fleet + ./scripts/freshness-dashboard.sh --root DIR use DIR as the parent holding the repos + +Consolidates vendoring drift, vendored-core integrity, and zsh/nvim plugin-pin freshness +with live GitHub signals (own-tag release drift, open dependency PRs, judgment-layer issue +links). Reporter only — never mutates, never fails the build; live signals need `gh` + a token. +EOF + exit 0 ;; *) echo "freshness-dashboard: unknown arg: $1" >&2; exit 2 ;; esac done diff --git a/scripts/test-core.sh b/scripts/test-core.sh index de593bb..901b881 100755 --- a/scripts/test-core.sh +++ b/scripts/test-core.sh @@ -1533,6 +1533,8 @@ else 'd=$(mktemp -d); cd "$d"; git -c init.defaultBranch=master init -q .; git commit -q --allow-empty -m x; [[ $(git_main_branch) == master ]]' gcheck "git_main_branch defaults to master when no known trunk exists" \ 'd=$(mktemp -d); cd "$d"; git -c init.defaultBranch=main init -q .; git commit -q --allow-empty -m x; git branch -m weirdtrunk; [[ $(git_main_branch) == master ]]' + gcheck "git_main_branch ignores a dangling origin/HEAD (stale after a remote rename)" \ + 'd=$(mktemp -d); cd "$d"; git -c init.defaultBranch=master init -q .; git commit -q --allow-empty -m x; git symbolic-ref refs/remotes/origin/HEAD refs/remotes/origin/main; [[ $(git_main_branch) == master ]]' fi # ── update.zsh per-manager parse (B5) ───────────────────────────────────────── diff --git a/scripts/update-nvim-plugins.sh b/scripts/update-nvim-plugins.sh index f4d5b02..c97e239 100755 --- a/scripts/update-nvim-plugins.sh +++ b/scripts/update-nvim-plugins.sh @@ -40,7 +40,14 @@ case "${1:-}" in --check) DRY=1 CHECK=1 ;; "") ;; -h | --help) - sed -n '21,26p' "$0" + cat <<'EOF' +update-nvim-plugins.sh — roll the pinned Neovim plugin revisions in nvim/lazy-lock.json. + + ./scripts/update-nvim-plugins.sh bump pins to latest, rewrite the lock + ./scripts/update-nvim-plugins.sh --dry-run show what WOULD change, restore the lock + ./scripts/update-nvim-plugins.sh --check like --dry-run but exit 2 if the lock is stale + (the lazy-lock half of the weekly freshness gate) +EOF exit 0 ;; *) diff --git a/scripts/update-plugins.sh b/scripts/update-plugins.sh index a74f3f1..3d5eef8 100755 --- a/scripts/update-plugins.sh +++ b/scripts/update-plugins.sh @@ -71,7 +71,14 @@ case "${1:-}" in --check) DRY=1 CHECK=1 ;; "") ;; -h | --help) - sed -n '15,24p' "$0" + cat <<'EOF' +update-plugins.sh — roll the pinned zsh-plugin SHAs in zsh/plugins.zsh to upstream HEAD. + + ./scripts/update-plugins.sh bump every pin to upstream HEAD, in place + ./scripts/update-plugins.sh --dry-run show what WOULD change, touch nothing + ./scripts/update-plugins.sh --check like --dry-run but exit 2 if any pin is stale + (the freshness gate .github/workflows/freshness.yml runs) +EOF exit 0 ;; *) diff --git a/starship/starship.toml b/starship/starship.toml index ad91ddf..6ac720a 100644 --- a/starship/starship.toml +++ b/starship/starship.toml @@ -77,6 +77,7 @@ $haskell\ $python\ $ruby\ $lua\ +$package\ [](fg:color_srf2 bg:color_srf1)\ $docker_context\ $kubernetes\ @@ -84,6 +85,7 @@ $aws\ $conda\ $pixi\ $direnv\ +$env_var\ $custom\ $jobs\ $time\ @@ -186,7 +188,7 @@ format = '([ $all_status $ahead_behind ]($style))' # text matching git's own diff colors. only_nonzero_diffs keeps it silent in a clean tree, # so the segment only appears once you've actually changed something. [git_metrics] -disabled = true +disabled = false added_style = "fg:color_green bg:color_srf2" deleted_style = "fg:color_red bg:color_srf2" format = '([ +$added ]($added_style))([ -$deleted ]($deleted_style))' @@ -262,7 +264,19 @@ format = '[[ $symbol($version) ](fg:color_aqua bg:color_srf2)]($style)' [python] symbol = " " style = "bg:color_srf2" -format = '[[ $symbol($version) ](fg:color_aqua bg:color_srf2)]($style)' +# Show the active virtualenv/venv name next to the version. The previous format +# was version-only, so a plain `python -m venv .venv && activate` project showed +# nothing about the env you're in. `(...)` is an optional group that renders ONLY +# when a venv is active, so a bare interpreter stays clean. venv name in purple. +format = '[[ $symbol($version) ](fg:color_aqua bg:color_srf2)]($style)([\($virtualenv\) ](fg:color_purple bg:color_srf2))' + +# Project package version indicator (package.json / Cargo.toml / pyproject / etc.) +# - the "package indicator" next to the language segments. Renders only inside a +# package directory, so it's silent elsewhere. Aqua, on the language surface. +[package] +symbol = "󰏗 " +style = "bg:color_srf2" +format = '[[ $symbol$version ](fg:color_aqua bg:color_srf2)]($style)' [docker_context] symbol = " " @@ -283,6 +297,19 @@ format = '[[ $symbol($context) ](fg:color_purple bg:color_srf1)]($style)' # installed by default on modern distros, so an ifconfig probe would silently never fire # there. A box lacking the probe binary just fast-fails and the segment hides — no timeout, # unlike Windows. +# -- OPT-IN: live Docker engine version (OFF by default - perf) -------------- +# docker_context above is the cheap STATUS (which daemon/context). A live VERSION +# means shelling out to the daemon every prompt in a Docker dir; command_timeout +# bounds it, but it can still add latency. Uncomment to enable: +# [custom.docker_version] +# description = "docker engine version" +# command = "docker version --format '{{.Server.Version}}'" +# when = "docker info" +# detect_files = ['Dockerfile', 'docker-compose.yml', 'docker-compose.yaml', 'compose.yaml'] +# symbol = "󰡨 " +# style = "fg:color_purple bg:color_srf1" +# format = '[ $symbol$output ]($style)' + [custom.vpn_macos] description = "active tunnel interface (macOS)" os = "macos" @@ -327,9 +354,19 @@ format = '[ $symbol ($profile) (\($region\)) ]($style)' [env_var.ENGAGEMENT] # show a custom env var when present (e.g. client tag) variable = "ENGAGEMENT" symbol = "🎯 " -style = "fg:color_red" +style = "fg:color_red bg:color_srf1" format = '[ $symbol $env_value ]($style)' + +# WSL indicator - shows the distro name when this shell is running inside WSL +# (WSL_DISTRO_NAME is exported by WSL). Silent on the native Windows host and on +# bare Linux/macOS. Blue, on the ops surface. Both env_var modules render via the +# $env_var slot wired into `format`. +[env_var.WSL] +variable = "WSL_DISTRO_NAME" +symbol = "󰌽 " +style = "fg:color_blue bg:color_srf1" +format = '[ $symbol$env_value ]($style)' [conda] style = "bg:color_srf1" format = '[[ $symbol ($environment) ](fg:color_purple bg:color_srf1)]($style)' diff --git a/tmux/tmux.conf b/tmux/tmux.conf index 41d5bac..7a17c1f 100644 --- a/tmux/tmux.conf +++ b/tmux/tmux.conf @@ -87,9 +87,9 @@ set -g status-style "bg=#{@tn_bg_hl},fg=#{@tn_bg_hl}" # normal → blue · prefix active → orange · copy/search mode → yellow set -g status-left-length 60 set -g status-left "\ -#[fg=#{?client_prefix,#{@tn_bg_dark},#{?pane_in_mode,#{@tn_bg_dark},#{@tn_bg_dark}}},bg=#{@tn_bg_hl}] #{@cap_l}\ +#[fg=#{@tn_bg_dark},bg=#{@tn_bg_hl}] #{@cap_l}\ #[bg=#{@tn_bg_dark},fg=#{?client_prefix,#{@tn_orange},#{?pane_in_mode,#{@tn_yellow},#{@tn_blue}}},bold]#{?client_prefix,󰠠 ,#{?pane_in_mode,󰆏 ,}}#S\ -#[fg=#{?client_prefix,#{@tn_bg_dark},#{?pane_in_mode,#{@tn_bg_dark},#{@tn_bg_dark}}},bg=#{@tn_bg_hl}]#{@cap_r} " +#[fg=#{@tn_bg_dark},bg=#{@tn_bg_hl}]#{@cap_r} " # ── WINDOWS: inactive = muted pill · current = blue pill (+ zoom glyph) ─────── set -g window-status-separator " " diff --git a/zsh/functions.zsh b/zsh/functions.zsh index 70d758c..c7a0640 100644 --- a/zsh/functions.zsh +++ b/zsh/functions.zsh @@ -746,13 +746,14 @@ pullall() { print -r -- "$output" - # Tally from the emoji markers. grep -c always prints exactly one integer (0 on no - # match), so these stay safe for the arithmetic below even when $output is empty. + # Tally the emoji markers in ONE awk pass instead of four grep -c scans of the same + # buffer. index() is byte-based (locale-independent for the multibyte glyphs) and, like + # grep -c, counts matching LINES; awk always prints four integers, so the arithmetic + # below stays safe even when $output is empty (0 0 0 0). local ok warn fail pruned - ok=$(printf '%s\n' "$output" | grep -c '✅') - warn=$(printf '%s\n' "$output" | grep -c '⚠️') - fail=$(printf '%s\n' "$output" | grep -c '❌') - pruned=$(printf '%s\n' "$output" | grep -c '🧹') + read -r ok warn fail pruned <<<"$(printf '%s\n' "$output" | awk ' + index($0,"✅"){o++} index($0,"⚠️"){w++} index($0,"❌"){f++} index($0,"🧹"){p++} + END{printf "%d %d %d %d\n", o, w, f, p}')" print print -r -- "${bold}📊 pullall summary${rst}" diff --git a/zsh/git.zsh b/zsh/git.zsh index 1bd3887..28437bb 100644 --- a/zsh/git.zsh +++ b/zsh/git.zsh @@ -19,19 +19,28 @@ # Current branch name (empty outside a repo / on detached HEAD failure). function git_current_branch() { local ref - ref=$(command git symbolic-ref --quiet HEAD 2>/dev/null) - local ret=$? - if [[ $ret != 0 ]]; then - [[ $ret == 128 ]] && return # not a git repo - ref=$(command git rev-parse --short HEAD 2>/dev/null) || return - fi - echo "${ref#refs/heads/}" + # `git branch --show-current` (git 2.22+) is the porcelain for the checked-out branch. + # It prints empty on a detached HEAD — fall back to a short SHA there — and stays empty + # outside a repo (rev-parse also fails), preserving both original behaviours. + ref=$(command git branch --show-current 2>/dev/null) + [[ -n $ref ]] && { echo "$ref"; return; } + command git rev-parse --short HEAD 2>/dev/null } # Resolve the trunk branch for this repo (main, master, trunk, ...). function git_main_branch() { command git rev-parse --git-dir &>/dev/null || return local ref + # Fast path: origin/HEAD names the remote's default branch authoritatively in ONE call + # (the usual case for a cloned repo) — no need to probe the candidate list. symbolic-ref + # succeeds even when origin/HEAD is DANGLING (points at a branch pruned/renamed upstream), + # so verify the referent actually exists before trusting it; a stale target falls through. + if ref=$(command git symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null) \ + && command git show-ref -q --verify "refs/remotes/$ref"; then + echo "${ref#origin/}" + return 0 + fi + # Fallback (origin/HEAD unset or stale): probe the known trunk names locally + on remotes. for ref in refs/{heads,remotes/{origin,upstream}}/{main,trunk,mainline,default,stable,master}; do if command git show-ref -q --verify "$ref"; then echo "${ref:t}" diff --git a/zsh/history.zsh b/zsh/history.zsh index 121352a..4ae1209 100644 --- a/zsh/history.zsh +++ b/zsh/history.zsh @@ -17,10 +17,8 @@ SAVEHIST=200000 # lines written to $HISTFILE [[ -d ${HISTFILE:h} ]] || mkdir -p "${HISTFILE:h}" setopt EXTENDED_HISTORY # write :start:elapsed;command (atuin import-friendly) -setopt INC_APPEND_HISTORY # append as you go, not just at shell exit -setopt SHARE_HISTORY # share across live sessions -setopt HIST_IGNORE_ALL_DUPS # drop older dup when a command repeats -setopt HIST_IGNORE_DUPS +setopt SHARE_HISTORY # share across live sessions (implies incremental append) +setopt HIST_IGNORE_ALL_DUPS # drop older dup when a command repeats (supersedes HIST_IGNORE_DUPS) setopt HIST_FIND_NO_DUPS # don't show dups when searching setopt HIST_IGNORE_SPACE # a leading space keeps a command out of history setopt HIST_REDUCE_BLANKS @@ -32,4 +30,4 @@ setopt HIST_SAVE_NO_DUPS # has its own richer filtering (history_filter in config.toml) — this is the # belt-and-suspenders for the flat file. Operator habit: prefix anything spicy # with a space (HIST_IGNORE_SPACE) and it never lands anywhere. -HISTORY_IGNORE='(pass show *|pass read *|pass insert *|*--password *|*--token *|*API_KEY*|*SECRET*|op read*)' +HISTORY_IGNORE='(pass show *|pass read *|pass insert *|op read*|*--password[ =]*|*--token[ =]*|*--api-key[ =]*|*PASSWORD=*|*TOKEN=*|*API_KEY*|*APIKEY*|*SECRET*|*ACCESS_KEY=*)' diff --git a/zsh/ui.zsh b/zsh/ui.zsh index 89cb04d..a4e8cfa 100644 --- a/zsh/ui.zsh +++ b/zsh/ui.zsh @@ -142,38 +142,50 @@ _core_errbox() { # Inputs are command/flag names (a handful of chars), so the O(n·m) table is trivial. # Pure zsh (1-based arrays under `emulate -L zsh`; scalar[i] yields the i-th char), so # it runs on a bare box with no awk/python. Powers _core_suggest's "did you mean?". +# _core_lev [outvar] → edit distance. With outvar, writes it into that variable +# (no subshell); without, prints on stdout. The outvar form lets _core_suggest score a +# whole candidate list without forking a command substitution per candidate. _core_lev() { emulate -L zsh - local a="$1" b="$2" - local -i la=${#a} lb=${#b} i j cost del ins sub trn m - ((la == 0)) && { print -r -- "$lb"; return; } - ((lb == 0)) && { print -r -- "$la"; return; } - # 1-based rows; index (k+1) holds column k (k = 0..lb). NOTE: array-element assignment - # subscripts must NOT contain spaces (`prev[j+1]=`, not `prev[j + 1]=`) — with spaces - # zsh parses the LHS as a glob word, not an assignment ("bad pattern: prev[j"). - # - # Damerau/OSA, not plain Levenshtein: we ALSO score an adjacent transposition as ONE - # edit (needs the row two back, prev2), so a finger-fumble like `gts`→`gst` or - # `cmod`→`comd` is distance 1 — the single most common real typo class — instead of 2, - # which _core_suggest's ≤2 cutoff would otherwise treat the same as two unrelated edits. - local -a prev2 prev cur - for ((j = 0; j <= lb; j++)); do prev[j+1]=$j; done - for ((i = 1; i <= la; i++)); do - cur[1]=$i - for ((j = 1; j <= lb; j++)); do - [[ "${a[i]}" == "${b[j]}" ]] && cost=0 || cost=1 - del=$((prev[j+1] + 1)); ins=$((cur[j] + 1)); sub=$((prev[j] + cost)) - m=$del; ((ins < m)) && m=$ins; ((sub < m)) && m=$sub - # adjacent transposition: a[i] matches b[j-1] AND a[i-1] matches b[j]. - if ((i > 1 && j > 1)) && [[ "${a[i]}" == "${b[j-1]}" && "${a[i-1]}" == "${b[j]}" ]]; then - trn=$((prev2[j-1] + 1)); ((trn < m)) && m=$trn - fi - cur[j+1]=$m + local a="$1" b="$2" outvar="$3" + local -i la=${#a} lb=${#b} i j cost del ins sub trn m res + if ((la == 0)); then + res=$lb + elif ((lb == 0)); then + res=$la + else + # 1-based rows; index (k+1) holds column k (k = 0..lb). NOTE: array-element assignment + # subscripts must NOT contain spaces (`prev[j+1]=`, not `prev[j + 1]=`) — with spaces + # zsh parses the LHS as a glob word, not an assignment ("bad pattern: prev[j"). + # + # Damerau/OSA, not plain Levenshtein: we ALSO score an adjacent transposition as ONE + # edit (needs the row two back, prev2), so a finger-fumble like `gts`→`gst` or + # `cmod`→`comd` is distance 1 — the single most common real typo class — instead of 2, + # which _core_suggest's ≤2 cutoff would otherwise treat the same as two unrelated edits. + local -a prev2 prev cur + for ((j = 0; j <= lb; j++)); do prev[j+1]=$j; done + for ((i = 1; i <= la; i++)); do + cur[1]=$i + for ((j = 1; j <= lb; j++)); do + [[ "${a[i]}" == "${b[j]}" ]] && cost=0 || cost=1 + del=$((prev[j+1] + 1)); ins=$((cur[j] + 1)); sub=$((prev[j] + cost)) + m=$del; ((ins < m)) && m=$ins; ((sub < m)) && m=$sub + # adjacent transposition: a[i] matches b[j-1] AND a[i-1] matches b[j]. + if ((i > 1 && j > 1)) && [[ "${a[i]}" == "${b[j-1]}" && "${a[i-1]}" == "${b[j]}" ]]; then + trn=$((prev2[j-1] + 1)); ((trn < m)) && m=$trn + fi + cur[j+1]=$m + done + prev2=("${prev[@]}") + prev=("${cur[@]}") done - prev2=("${prev[@]}") - prev=("${cur[@]}") - done - print -r -- "${prev[lb+1]}" + res=${prev[lb+1]} + fi + if [[ -n $outvar ]]; then + : "${(P)outvar::=$res}" + else + print -r -- "$res" + fi } # _core_suggest → echo the single CLOSEST candidate when it's a @@ -185,7 +197,7 @@ _core_suggest() { local best="" c local -i d bestd=99 for c in "$@"; do - d=$(_core_lev "$input" "$c") + _core_lev "$input" "$c" d # writes the distance into $d, no subshell ((d < bestd)) && { bestd=$d; best=$c; } done ((bestd <= 2 && bestd < ${#input})) && [[ -n "$best" ]] && print -r -- "$best" From b91ae4c6582cb1b218f8beb5dc0683aee4ee2f7a Mon Sep 17 00:00:00 2001 From: dotfiles-core sync bot <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 21:55:30 +0000 Subject: [PATCH 2/2] =?UTF-8?q?chore(core):=20core.lock=20=E2=86=92=20e9cf?= =?UTF-8?q?907f5849=20(v3.7.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/core.lock b/core.lock index b6c926e..e0697b7 100644 --- a/core.lock +++ b/core.lock @@ -1,6 +1,6 @@ # GENERATED by dotfiles-core sync-core.sh — vendored Core provenance (B1). # Regenerate after a manual 'git subtree pull' with: make core-lock -core_version=3.6.1 -core_sha=3ace724a75c6163f5bf22861fb844b67ff80a347 -core_branch=3ace724a75c6163f5bf22861fb844b67ff80a347 -core_tag=v3.6.1 +core_version=3.7.0 +core_sha=e9cf907f5849dfae2f3313d840fcbafb3772b83a +core_branch=e9cf907f5849dfae2f3313d840fcbafb3772b83a +core_tag=v3.7.0