# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- **`help:OP` op prints the full reference for a single op** — `help:vim` emits that op's syntax, uncompacted description, and example, read straight from `.supertool.json` (builtin-ops → custom ops → aliases). The motivation: op hints and the `vim` example line told the reader to "see `./supertool help vim`", but no `help` op existed — `./supertool help vim` returned `unknown operation: help`, a dead front door, so discovering a non-obvious payload shape (vim's macro grammar, the `@file` field names) meant grepping source. `help` reuses the existing config as the single source of truth — the same metadata `ops` lists, but scoped to one op and never compacted, so a long description (vim's is ~1.5KB) prints in full instead of being dropped by `ops-compact`'s self-explanatory filter. An unknown op errors with a pointer to `ops`; a real built-in with no config entry says "has no documented help" rather than "unknown operation", distinguishing the two. Read-only and parallel-safe. Use `help:OP` when you know the op but not its payload; use `ops` when you don't yet know the op. Closes [#341](https://github.com/Digital-Process-Tools/claude-supertool/issues/341).
- **`cwd:PATH` op sets the working directory for a whole call** — `cwd:~/repo` as the *first* op chdir's once before any dispatch, so every following op resolves against `PATH`, then it's stripped (it never reaches the dispatch loop). Mirrors `cd PATH && ./supertool …` without the `cd`: a line starting with `cd` trips the `use-supertool` hook, and a forgotten/stale `cd` silently poisons relative path resolution (the exact trap that made `git-diff:.supertool.json` report "No changes" against the wrong clone in #336). Cross-repo sessions — a product repo plus the supertool clone — otherwise needed a `cd <repo> &&` prefix on every other call. It's an op, not a `--flag`, to keep supertool's single grammar; handled in the pre-pass (like `--plain`) so it can't race the parallel read path or force a batch sequential. Required-first on purpose: an op after it would have already resolved against the old dir, so a mid-call `cwd:` is rejected (`must be the first op`) rather than silently half-applied. `~`/`$VAR` are expanded; a non-directory target errors before any op runs. Closes [#339](https://github.com/Digital-Process-Tools/claude-supertool/issues/339).
- **Validators/formatters gained a per-spec `exclude` glob** — a validator or formatter spec may now set `"exclude": "*tests/*"` (or a list of globs, skip-if-any-matches) to skip files even when its `match` glob hits. The motivating case: editing a PHPUnit test file fired the `phpmd` validator, which reported `TooManyPublicMethods` (every `testXxx()` is public) — pure noise, since no real gate scans tests (CI mess-detection scans the source dir only; the pre-push git-hook skips any `/tests/` path). Validators were gated by a single positive `match` glob with no exclude, so there was no config-only fix. `exclude` is per-spec on purpose: a blanket "skip tests" would break `phpunit`, which *must* run on test files. Generalizes the prior `PSR_EXCLUDE` env hack into a first-class config key. Closes [#335](https://github.com/Digital-Process-Tools/claude-supertool/issues/335).
- **`gl-mr` now lists the per-file name-status by default** — the MR dashboard prints a `## Files (N)` block with one `A`/`D`/`R`/`M` line per changed path, sourced from the paginated `merge_requests/:iid/diffs` endpoint (so the change type comes straight from GitLab's `new_file`/`deleted_file`/`renamed_file` flags, no git shellout). "What got removed?" is the high-signal question when reviewing an MR, and previously `gl-mr` gave only a file *count* — forcing a separate `git diff --name-status master...branch` round-trip, exactly the borrowed round-trip the variants exist to kill (the concrete trigger was auditing a deleted-migration concern on a real MR). The list is capped at 50 files with a `… +N more` marker so large MRs don't blow context; `gl-mr:N:full` uncaps it (paginating up to 500 files) alongside the existing comment uncap. Any API/parse failure silently omits the block. Closes [#332](https://github.com/Digital-Process-Tools/claude-supertool/issues/332).
- **`git-status` gained a `:full` mode** — `git-status:full` (alias `:porcelain`) uncaps every list in the dashboard (staged/unstaged/untracked files, other branches, stashes), printing the complete untruncated set instead of the default `... (N more)` markers. The default view stays capped (20 staged/unstaged, 10 untracked/branches, 5 stashes) — a cheap overview that answers ahead/behind + dirty + MR. The bug wasn't the truncation (correct for the common case) but the lack of an escape hatch: driving precise staging — e.g. excluding a few pre-existing untracked items from a large commit — needs the full machine-readable list, which previously forced a drop back to raw `git status --porcelain`. Closes [#330](https://github.com/Digital-Process-Tools/claude-supertool/issues/330).
- **`gl-job` / `gh-job` gained a `:fail` suffix** — `gl-job:ID:fail` (alias `:errors`) prints only the matched failure blocks with no tail, the tight triage view. It applies the same per-job pattern table as the default mode (rector → diff lines, phpstan → 🪪/type markers, unit → `FAILURES!`/`Failed asserting`), so a red job shows just its actionable failures instead of the default's blocks-plus-80-line-tail. This names the discoverable front door for a behavior that previously only existed as the undocumented `:errors` mode on `gl-job` (and was entirely absent on `gh-job`); `:errors` stays as a back-compat alias. The default (no suffix), `:grep:PATTERN`, and `:raw` are unchanged. Closes [#326](https://github.com/Digital-Process-Tools/claude-supertool/issues/326).
- **`grep` gained a `:no-auto-read` flag** — `grep:PATTERN:PATH:no-auto-read` suppresses the single-small-file auto-read so only the matching line(s) are emitted, mirroring `glob`'s existing flag. Default behavior (auto-read a concrete file < 20KB on a match) is unchanged. The flag is order-independent with `:count` and any `LIMIT`/`CONTEXT` args. Avoids silently dumping 10-18KB of unrequested file content when the caller only wants the hit. Closes [#320](https://github.com/Digital-Process-Tools/claude-supertool/issues/320).
- **`git-commit` gained an `@file` payload route for multi-line messages** — `git-commit:@-` (stdin) or `git-commit:@msg.toml` reads a `message` field (subject + blank line + body) and an optional `paths` list, so a real subject+body commit no longer forces a drop to raw `git commit -F file` — which skipped the op's auto `Co-Authored-By` trailer and needed a follow-up `--amend`. The trailer is still appended on the payload route. The colon-CLI one-liner (`git-commit:::MESSAGE`) is unchanged. Mechanically this generalizes the `@file` field parser: a syntax token in a trailing `[...]` group is now optional, and a `...` token is variadic (a payload list expands into multiple positional args) — previously the parser leaked the `[`/`...]` brackets into the field names (`msg[`, `path...]`), so the route 404'd with `missing required field 'msg['`. Closes [#340](https://github.com/Digital-Process-Tools/claude-supertool/issues/340).
- **Generalized the new-class-without-test advisory into a config-driven `advice` block** — the hardcoded `adviseForNewTest` bool is replaced by a top-level `advice` map of named rules, each gated (all optional) by `hooks_into` (ops, default all mutating), `match` (path glob), `when` (`new-file`|`existing-file`|`always`), `contains` (regex over the content the op *added* — lines present after but not before, so it fires only when the op introduces the pattern), and `resolve`/`resolveFromValidator` (a subprocess emitting a would-be target via the same exit-3 + stderr contract). `message` is the rendered line, with `{target}`/`{path}`/`{op}` interpolation. The old single-purpose advisory was a dead end the moment a second nudge was wanted (e.g. "regenerate the XSD/cache after adding a component class or public attribute" — exactly the easy-to-forget step the XML editor depends on); the generic block lets a project add that as data instead of code. **Breaking:** the `adviseForNewTest: true` bool is removed — express it as an `advice.newTest` rule (`{"hooks_into": ["paste"], "match": "*.php", "when": "new-file", "resolveFromValidator": true, "message": "new class without test"}`); see `.supertool.example.json`. Closes [#347](https://github.com/Digital-Process-Tools/claude-supertool/issues/347).

### Fixed

- **Two `@-` (stdin) ops in one call now error clearly instead of an opaque parse failure** — `./supertool 'edit:@-' 'edit:@-'` made both ops call `sys.stdin.read()`; the first drained the single stream, so the second read empty and died with `@file ... parse error (<stdin>): Expecting value: line 1 column 1`, naming neither the real cause (the stream was already consumed) nor the fix. main() now detects more than one `op:@-` in a pre-pass — sibling to the existing `cwd:` pre-pass, before any dispatch — and exits with `stdin: only one '@-' op is allowed per call (got N: ...)`, listing the offending ops and pointing at the two escape hatches: give all but one a real `@file` path, or fold them into a single `batch:@-` ops array. A lone `@-` (the common case) is unchanged; a bare `@-` arg with no `op:` prefix isn't counted. Closes [#341](https://github.com/Digital-Process-Tools/claude-supertool/issues/341).
- **An MCP server's own timeout no longer surfaces as a phantom `+1` diagnostic** — after an edit, `lsp-diag` reported a `0 → 1` count where the single "diag" was actually `orchestrator timeout after 3s`, an infrastructure condition, not a code finding (phpstan had already covered the same file clean). The cause: cclsp swallows its *internal* LSP-orchestrator timeout and returns it as normal MCP text content with the `isError` flag unset — so it flowed through the shared dispatch as a real result and the lsp-diag adapter counted it as an advisory, reading like the edit introduced a regression. Detection now lives in the shared `_mcp_call_or_message` (so every MCP-backed op — `diag`/`hover`/`rename` — benefits, not just lsp-diag) with two signals: the structural MCP `isError` flag (spec-standard, any server) and a configurable per-server `infra_patterns` substring list (default `["orchestrator timeout", "timed out after"]`) for servers that report timeouts as plain text. A matched result is returned prefixed `op: …` — the same shape as supertool's own MCP errors — so the adapter's existing `op:`-prefix guard drops it instead of counting it. No count delta, no false regression, and the timeout value itself stays per-server configurable via `mcp.<name>.timeout`. Closes [#346](https://github.com/Digital-Process-Tools/claude-supertool/issues/346).
- **`rector-mcp` engine-glitch suppression is config-driven and now catches `toMutatingScope() on null`** — the warm rector daemon intermittently trips a PHP fatal inside rector's own engine (`Call to a member function toMutatingScope() on null`), a non-deterministic glitch a cold `rector` CLI does not reproduce. Like the June `System error: ClassReflection` case it surfaced as a false red *and* — because the validator cache keys on file content — froze into the cache and replayed on every later run until the file changed (the 2100-poisoned-entries failure mode, new signature). The fix moves glitch knowledge out of the core and into the adapter: the core had been string-matching `System error:` in `_validator_result_is_cacheable`, violating `SCHEMA.md`'s "validator core never parses tool-specific output". That branch is removed — the core cache filter is now fully generic, keying only off non-deterministic error *codes*. Tradeoff: the core no longer has a message-level backstop, so a brand-new, uncatalogued glitch signature can still reach the cache as a `rector.error` until it is added to the config list — the same failure mode, now bounded to *unknown* signatures and fixed by a one-line config edit rather than a code change. Signatures live as a config prop, `validators.rector.engine_glitches` in `.supertool.json` (a JSON list of case-sensitive substrings the adapter reads straight from `.supertool.json` — no env, the same file `daemon.py` already reads from cwd), defaulting to `["System error:", "toMutatingScope() on null"]` when absent. The adapter drops a matching error at the source, so a glitch never surfaces or reaches the cache; a new signature is a one-line config edit, not a code change. Part of [#345](https://github.com/Digital-Process-Tools/claude-supertool/issues/345).
- **`git-diff:PATH` no longer reports a missing/untracked path as "No changes."** — a path absent or untracked in the current repo produced an empty diff that printed `No changes.`, indistinguishable from a clean tracked file. Combined with a stale CWD (e.g. a `cd` into another repo) this is a silent false-negative: the op reads as "nothing changed" when it actually looked in the wrong place. Path mode now checks `git ls-files` first — a missing path warns `not found under <cwd> — wrong CWD?` and exits 1, an untracked on-disk path warns `untracked (not in git)`, and only a genuinely clean tracked file still says `No changes.`. Every mode also stamps a `Repo: <toplevel>` header so a wrong-repo invocation is visible at a glance (the trigger: `git-diff:.supertool.json` silently returned "No changes" while anchored to the wrong clone). Closes [#336](https://github.com/Digital-Process-Tools/claude-supertool/issues/336).
- **`grep` path-not-found error now includes the CWD** — `ERROR: path not found: <path> (cwd: <cwd>) — wrong CWD?` instead of the bare path, so the recurring "stale cwd makes a relative path silently miss" trap is diagnosable from the message alone. Part of [#336](https://github.com/Digital-Process-Tools/claude-supertool/issues/336).
- **`git-commit` no longer aborts on an already-staged deletion** — listing a `git rm`'d path (gone from disk) alongside present files made the op's `git add` step fail with `pathspec did not match any files`, killing the whole commit. The op now drops paths that are already staged as deletions from the `git add` step (their deletion is already staged and gets committed), so a mixed changeset of modifications + new files + deletions lands in one commit. Genuinely-unknown paths still error as before. Closes [#324](https://github.com/Digital-Process-Tools/claude-supertool/issues/324).

## [0.19.0] - 2026-06-24

### Added

- **`validate` op gained a list form — `validate:f1,f2,…:tool_filter`** — validating several files in one invocation (config loaded once for the whole batch). Single-file `validate:PATH` is unchanged. Each path in the list is independently security-checked at dispatch. Closes [#306](https://github.com/Digital-Process-Tools/claude-supertool/issues/306).
- **Declarative `@syntax` validator filter** — `validate:PATH:@syntax` selects validators that set `"syntax": true` in their spec, keeping the parser/compiler scope in config instead of a hardcoded caller list.
- **`git-resolve` per-hunk side selection via an optional `BLOCKS` arg** — `git-resolve:::SIDE:::PATH:::1,3` resolves only the listed 1-indexed conflict blocks (numbered as `git-conflicts` lists them); the rest stay conflicted. No block list = whole-file behavior unchanged. Honors the marker hard-gate: a partial resolve reports "N of M blocks resolved, file still conflicted" and never stages the file. Mixed sides per call are out of scope (v1). Closes [#305](https://github.com/Digital-Process-Tools/claude-supertool/issues/305).
- **`git-commit` auto-appends a configurable `Co-Authored-By` trailer** when the message lacks one. The value comes from `SUPERTOOL_COAUTHOR` (default `Max <noreply>`); disable with an empty value or `none`/`off`/`false`. Closes [#286](https://github.com/Digital-Process-Tools/claude-supertool/issues/286).
- **Plain / ASCII output mode for hooks, `grep` and CI.** Pass `--plain` (or set `SUPERTOOL_PLAIN=1`) to emit ASCII-only output — `[WARN]` / `[OK]` / `[FAIL]` / `[INFO]` in place of the `⚠` / `✓` / `✗` / `ℹ` glyphs — so downstream consumers parse reliably without UTF-8/locale assumptions (a C/POSIX-locale `grep` won't match a multibyte glyph; a cp1252 console crashes on one). Stable ASCII section keys (`Red flags in added lines`, `Forbidden paths`, …) stay intact for grepping. The `--plain` flag exports `SUPERTOOL_PLAIN=1` so preset ops (run as subprocesses) inherit it. Stdout/stderr are also defensively reconfigured to UTF-8 at startup, so a stray glyph in diffed content never raises `UnicodeEncodeError` on a non-UTF-8 console — even with plain mode on. Default (rich) output is unchanged. Closes [#308](https://github.com/Digital-Process-Tools/claude-supertool/issues/308).

### Changed

- **Validator receipts now label pre-existing errors instead of an ambiguous `(unchanged)`.** When a mutating op leaves the error count unchanged from a pre-existing baseline, the row reads `(pre-existing — not from this edit)` and lists up to 5 of the offending errors, so a failure that predates the edit is no longer misread as caused by it. Closes [#307](https://github.com/Digital-Process-Tools/claude-supertool/issues/307).
- **`git-resolve`'s post-resolve syntax digest now shells `validate` ONCE for all resolved files** instead of once per file, via the new list form. It uses the declarative `@syntax` scope (falling back to the hardcoded name list for older configs) and folds the per-file rows back into each receipt line. Closes [#306](https://github.com/Digital-Process-Tools/claude-supertool/issues/306).
- **`read` op now hints that a following supertool `edit` needs no preceding harness `Read`** — supertool's read is a Bash subprocess that never registers with the harness must-Read-first gate, so the hint steers callers to `edit`/`replace`/`vim` (which validate independently) rather than the harness `Edit`, which would fail with "File has not been read yet". Closes [#309](https://github.com/Digital-Process-Tools/claude-supertool/issues/309).
- **`git-commit`'s "Next:" hint recommends the `git-push` op** instead of raw `git push`, so the suggested follow-up carries the op's ahead/behind and mergeability receipt. Closes [#310](https://github.com/Digital-Process-Tools/claude-supertool/issues/310).

### Fixed

- **`gl-job`/`gh-job` `:grep:` regex fallback message reworded to a neutral `(literal match)`.** An unbalanced-paren pattern (e.g. `1)`) that fails to compile as a regex falls back to a literal substring search; the message no longer reads like an error. Closes [#299](https://github.com/Digital-Process-Tools/claude-supertool/issues/299).

## [0.18.0] - 2026-06-23

### Fixed

- **`git-push` no longer misreports a pre-push hook's amend-and-push as `PUSH REJECTED`.** A pre-push hook that auto-formats commonly amends HEAD, pushes the corrected commit itself, then exits non-zero so git won't also push the stale pre-amend ref — that non-zero is *success*, not failure. `git-push` now trusts the live remote SHA (`ls-remote`) over the exit code: when the remote already matches local HEAD it reports `PUSHED (pre-push hook amended HEAD)` with the rewritten SHA, instead of `REJECTED`. And `_first_error_line` no longer picks a green `✅ … 0 errors` / `pushed successfully` line as the "first error" (the substring `errors` was matching the error scan). Closes [#297](https://github.com/Digital-Process-Tools/claude-supertool/issues/297).

### Added

- **`git-push` embeds non-fast-forward recovery.** When the remote has moved ahead, `git-push` now fetches, surfaces the **incoming remote commits** (short SHA, author, subject — the signal for force-vs-integrate, since forcing over a teammate's commit destroys it) with the ahead/behind counts, then rebases your work onto the remote and re-pushes in the same call — no manual `pull --rebase` → `git-push` round-trip. Computing is cheap; LLM round-trips are not. On conflict it leaves the rebase **paused** (never silently aborted or force-pushed), lists the conflicting files, warns to check the incoming authors before forcing, and points at `git-conflicts` to inspect plus the exact keep-both / cancel (`git rebase --abort`) / force paths — the resolution stays the caller's decision.
- **`git-push` post-push receipt surfaces the next decision.** A push isn't the end of an action, it's the start of "get this MR merged" — so the receipt now carries the signals you'd otherwise spend round-trips discovering, all riding calls already made: **mergeability** (warns loudly if the MR now `cannot_be_merged` with its target — caught at push time, not 20 min later at merge), **stale base** (`N commit(s) behind origin/<target>`), **uncommitted leftovers** (`git status --porcelain` — the "I fixed two things, committed one, pushed" catch), the **pipeline id + url** on the MR line, and a ready **`watch:gitlab-mr:<iid>`** command to hand the pipeline to a background poller. `:force-with-lease` additionally reports **what it discarded** (author + subject of the overwritten remote commits — the receipt for "did I just nuke a teammate's work"). Watching stays opt-in: the receipt recommends the command by default; `git-push:watch` spawns the poller for the push you're actually iterating on (no stray daemons on WIP pushes).
- **`git-push:force-with-lease` and `git-push:no-verify` variants.** `:force-with-lease` is the safe force (overwrite only if the remote hasn't moved since you fetched) and suppresses the auto-rebase, since the force is your explicit decision; `:no-verify` skips the local pre-push hook, the documented escape when a local formatter legitimately diverges from CI. The op cmd switched from `{arg}` to `{args}` so the flag tokens reach the script. Part of [#297](https://github.com/Digital-Process-Tools/claude-supertool/issues/297).

## [0.17.0] - 2026-06-23

### Added

- **`gl-pipeline` filter modes — `:active` and `:failed`.** Polling a running pipeline across turns re-paid ~90 lines of `manual`/`created` bulk every call. Two filter modes cover the only questions you ask mid-pipeline: `gl-pipeline:ID:active` shows just running/pending jobs ("what's still going"), `gl-pipeline:ID:failed` shows just the failed jobs plus their job IDs/URLs ("what broke"). The default board now collapses the `manual`/`created`/`skipped` bulk to a one-line count (`+60 manual, +10 created (hidden — …)`) instead of one row each, so the running/failed/done jobs aren't buried. The op cmd switched from `{arg}` to `{args}` so the filter token reaches the script. Closes [#298](https://github.com/Digital-Process-Tools/claude-supertool/issues/298).

## [0.16.0] - 2026-06-22

### Added

- **`git-push` op — update the branch behind an already-open MR.** The `mr` op covers push **and** create; there was no op for the common follow-up — pushing a fix to an MR that already exists — so it dropped to raw `git push -u origin HEAD`. `git-push` pushes the current branch (setting upstream on first push), then prints a verifiable receipt: remote SHA before/after with commits pushed (or "already up to date" / "branch created"), ahead/behind vs upstream, and the open MR/PR for the branch with its pipeline status (the push triggers a run). Non-fast-forward rejections surface a `pull --rebase` / `--force-with-lease` hint; server-side rejections (protected branch / pre-receive hook) get a distinct "a rebase won't help" message instead — never auto-forced, mirroring `git-commit`'s no-silent-bypass philosophy. The glab→gh MR lookup that was drifting across `commit.py` / `push.py` is now a shared `presets/git/_git_common.py` (`query_open_mr`, plus `_git` / `_first_error_line`). Closes [#288](https://github.com/Digital-Process-Tools/claude-supertool/issues/288). See [docs/presets/git.md](docs/presets/git.md).

## [0.15.0] - 2026-06-22

### Added

- **`gl-job` / `gh-job` `:grep:PATTERN` + per-job failure patterns with resolution ops.** `gl-job:ID:grep:PATTERN` (and the `gh-job` twin) runs an ad-hoc regex over the job trace — context window, gap markers, regex with literal fallback on `re.error`, and never silent-empty (no match names the pattern and shows a tail). Complements the existing `:raw` slice when you don't yet know the line numbers. Plus an optional per-job-name table in `.supertool.json`: `ops.gl-job.job_patterns` maps a job-name regex to tighter `patterns` and a `resolution` op — a failed `rector` job ends with `Resolve: ./supertool 'rector_ci_apply:<id>'` (`{id}` interpolated). No match falls back to the flat `error_patterns`. Two enablers: non-scalar custom-op config values are now JSON-encoded into their `SUPERTOOL_*` env var (so presets can `json.loads` structured config), and project-level `ops` overrides **deep-merge** into preset op-defs key-by-key (add `job_patterns` without restating the preset `cmd`). Closes [#289](https://github.com/Digital-Process-Tools/claude-supertool/issues/289). See [docs/presets/gitlab.md](docs/presets/gitlab.md).
- **`restartMcp` key on custom ops.** A custom op that invalidates state the warm MCP daemons cache (autoload map, PHPStan/PHPUnit result caches, …) can declare `"restartMcp": true` (every server in the `mcp` block) or `"restartMcp": ["name", …]` (a subset). After the op's `cmd` **succeeds**, supertool SIGTERMs those daemons via the same `stop.py` path the new-file invalidation uses; each cold-starts on its next call with a fresh index. Skipped when the cmd fails. Lets a single cache-clearing op (e.g. `dvsi_clearcache`) also reset the warm LSP layer in one round-trip, instead of leaving daemons serving stale analysis until the idle timeout.
- **`gl-mrs` — MR triage board.** Lists MRs (default `author=@me`), sorted failing-first then stalest, each enriched in parallel with everything you'd otherwise round-trip for: pipeline status (a failure shows the failed **job name** — `phpstan2`, `test_unit_dpt` — i.e. the failure class, pre-empting a `gl-job` call), approval state, age, diff size, watch-state cross-reference (which MRs already have a live `watch` poller), and `conflict`/`draft`/`threads` flags. Footer points at the first failing-and-unwatched MR. Filters: `author`/`reviewer`/`assignee`/`label`/`milestone`/`state`/`per`. Flags: `nopipe`, `iids` (bare id list), `failed` (only failing). Tunable via `enrich_workers`/`enrich_cap`/`per_page`. "Mine" is just the default filter, not a special op — enumeration is a platform concern, kept out of the generic `watch` preset. See [docs/presets/gitlab.md](docs/presets/gitlab.md).
- **`gh-prs` — PR triage board (the `gl-mrs` twin).** Lists GitHub PRs (default `author=@me`), sorted failing-first then stalest. Per PR: check rollup (a failure shows the failing **check name** — the failure class, pre-empting a `gh-job` call), approval state (`reviewDecision`), age, diff size, watch-state cross-reference, and `draft`/`conflict`/`threads` flags. Footer points at the first failing-and-unwatched PR. Unlike GitLab, `gh pr list --json` returns checks + approval in **one** call, so the core board costs a single request; only unresolved review-thread counts enrich in a second parallel wave (skip with `nopipe`). Filters: `author`/`reviewer`/`assignee`/`label`/`state`/`per`. Flags: `nopipe`, `iids` (bare number list), `failed`. Closes [#274](https://github.com/Digital-Process-Tools/claude-supertool/issues/274). See [docs/presets/github.md](docs/presets/github.md).
- **`presets/watch/watch-mine.sh` — watch-a-whole-query supervisor.** Glue between a "list mine" op (`gl-mrs`/`gh-prs`) and the `watch` preset: runs the feed op, extracts ids, spawns one watcher each. Idempotent, so it's safe on `/loop`. `gl-mrs:failed,iids | watch` turns "ping me when any of my MRs goes red" into one looped command. See [docs/presets/watch.md](docs/presets/watch.md).
- **Configurable default `project` / `repo` for issue-create payloads.** `gl-issue-create` and `gh-issue-create` no longer require `project`/`repo` in every payload. Resolution order (most specific wins): explicit payload field → `defaults.gitlab_project` / `defaults.github_repo` in `.supertool.json` → the `origin` git remote, host-matched (`gitlab` / `github.com`). In the matching checkout the remote covers it with zero config. Closes [#270](https://github.com/Digital-Process-Tools/claude-supertool/issues/270). See [docs/presets/gitlab.md](docs/presets/gitlab.md) and [docs/presets/github.md](docs/presets/github.md).
- **`watch` preset — background event pollers with async wake into Claude Code.** New ops `watch:SOURCE:ID[:only=...]`, `unwatch:SOURCE:ID`, `watches`. Each invocation forks a detached poller that emits state-change events to a UDS socket, a status file, and macOS Notification Center. Source-plugin contract makes new sources ~50 lines. Bundled sources: `github-pr` (PR state, checks, reviews, comments, merge/close, conflicts) and `gitlab-mr` (MR state, pipeline, merge/close, conflicts). Closes [#165](https://github.com/Digital-Process-Tools/claude-supertool/issues/165). See [docs/presets/watch.md](docs/presets/watch.md).
- **`notifiers/claude-channel/` — MCP channel server for async wake.** TypeScript / Bun server that binds the watch UDS socket and pushes events into a running Claude Code session via the Channels feature (research preview, requires Claude Code v2.1.80+). Events arrive as `<channel source="claude-channel" watcher_source="<source>" id="<id>" event="<event>" ...>` tags. UDS file mode 0600 + localhost-only auth model. Launch with `claude --dangerously-load-development-channels server:claude-channel`. See [notifiers/claude-channel/README.md](notifiers/claude-channel/README.md).
- **`glob` brace expansion.** `glob:src/**/*.{json,xml}` now fans out into `*.json` + `*.xml` (shell/fd/ripgrep semantics) and dedupes results, instead of silently returning 0 files. Supports multiple groups (`{a,b}.{x,y}` → 4) and nesting (`{a,b{1,2}}` → 3). Patterns without braces are unchanged. Closes [#161](https://github.com/Digital-Process-Tools/claude-supertool/issues/161).
- **`validator_cache_ttl_hours` config** (default `24`, `0` disables). Validator cache entries expire this long after being written. The cache key only hashes file content, so without a TTL an entry outlives changes it can't see — an updated adapter/`rector.php`, or a transient engine failure. Expiry is on access (stale → miss → re-run → rewrite); no cron needed.

### Fixed

- **Test suite no longer mutates the repo it runs in (corrupted working trees under the pre-push hook).** `TestGitcliFlagSmuggling::test_checkout_allows_dash_alone` ran `checkout.py -` via a `subprocess.run` with no `cwd=`, so it executed a real `git checkout -` (switch to previous branch) against whatever directory pytest was launched from. Harmless under a normal `pytest` invocation from a stable branch — but `.githooks/pre-push` runs the **full suite** on every push, so each `git push` from a feature branch silently switched the live repo to its previous branch mid-push, moving `HEAD` and reverting the working tree. Combined with a git wrapper that reports `ok` regardless of the push result, this presented as "force-push silently failed" / "HEAD reset to master" and corrupted in-flight work twice in one session before being traced. Fix: `_run_git_preset` gained a `cwd` parameter and the test now builds an isolated throwaway repo (with a real previous-branch reflog) and asserts `-` switches to it — verified by re-running the full suite from a clone and confirming `HEAD` is unchanged.
- **`claude-channel` MCP server now loads from any project directory.** The bundled `.mcp.json` launched `bun` with a cwd-relative script path (`./notifiers/claude-channel/channel.ts`). Claude Code spawns plugin MCP servers with the working directory set to the *active project*, not the plugin root, so the path resolved against the wrong directory and the server failed to connect everywhere except the plugin dir itself (`Module not found` → `-32000 Connection closed`). Switched to the `${CLAUDE_PLUGIN_ROOT}/…` variable Claude Code expands in plugin `.mcp.json` files, matching what `notifiers/claude-channel/install.sh` already wrote with an absolute path. Also aligned the version mismatch the reporter flagged: `plugin.json` (`0.13.1`) vs `supertool.py` `VERSION` (`0.14.0`) → both now `0.14.1`, with a test asserting they stay in sync. Closes [#279](https://github.com/Digital-Process-Tools/claude-supertool/issues/279). Thanks [@riccardoshrives](https://github.com/riccardoshrives) for the detailed report and verified fix.
- **Validator cache no longer freezes transient engine failures.** `rector-mcp`'s warm daemon intermittently trips rector's own `System error: "ClassReflection must be resolved for class X"` reflection bug on test classes — warm-process-state dependent, not file dependent (a clean re-run and plain `rector` CLI pass the same file). That failure was cached keyed on file content and replayed on every later run, including its frozen `duration_ms`; 2100 entries were poisoned this way across a test suite. Two-layer fix: (1) `validators/rector-mcp/rector-mcp.py` drops `System error:` results at the source — an engine glitch is not a code finding; (2) the validator cache now excludes non-deterministic engine failures (MCP transport errors, non-zero exits, `System error:` messages) from being written. Real findings (PHPStan types, `rector.refactor`) stay cached.
- **`git-merge` now fetches before merging a local branch — no more silent stale merges.** The docstring promised "fetch + merge" but there was no fetch: `git-merge master` merged the *local* `master` ref, which is stale the moment origin moves ahead. Real-world bite: a fix landed on origin/master, but merging `master` into a feature branch silently used the lagging local ref and the fix "wasn't there" — the failing job kept failing. Now `_fresh_merge_ref()` resolves the ref's upstream, fetches it, and if the local branch is behind, redirects the merge to the upstream (e.g. `origin/master`) with a loud note: `local master was N behind origin/master — merging origin/master (latest) instead`. Offline / fetch-fail / no-upstream cases warn and fall back to the local ref (no hard failure). Remote-tracking refs like `origin/master` are refreshed directly.

### Changed

- **`rector-mcp`'s `System error:` suppression is now defense-in-depth, not load-bearing.** The root cause behind the warm `ClassReflection must be resolved for class X` failures ([#273](https://github.com/Digital-Process-Tools/claude-supertool/issues/273)) is fixed upstream in `dpt/mcp-rector-warm` 0.4.0: the warm daemon reused one Rector container and never reset Rector's `DynamicSourceLocatorProvider`, which caches its source locator for every non-PHPUnit run — so the second file in a warm session was analysed with a locator that only knew the *first* file. The daemon now resets reflection state per call (matching `AbstractRectorTestCase`), so this branch should no longer fire. The adapter comment was updated to reflect that it remains only to absorb any future non-deterministic engine glitch. **Requires `dpt/mcp-rector-warm` ≥ 0.4.0** (`composer global update dpt/mcp-rector-warm`).

## [0.14.0] — 2026-05-23

### Added

- **`validators/pyright/`** — Python type-check via `pyright --outputjson`. Sits next to `py-compile` (syntax-only). Hooks into `edit/replace/replace_lines/paste/vim` for `*.py`. Graceful skip when `pyright` not on PATH; pyright JSON 0-indexed `range.start` is converted to supertool's 1-indexed `line/col`. Configure via `.supertool.example.json`. README validator count bumped to 18.

### Security

- **`op_paste` containment ordering.** Previously `op_paste` called `os.makedirs(parent)` BEFORE `_atomic_write`'s `_safe_path` check, so a traversal path like `../../tmp/evil/foo` would create directories outside cwd before the write itself was rejected — leaving empty-dir pollution. `op_paste` now runs `_safe_path` at entry. Side benefit: closes the NUL-byte-in-path xfail (`_safe_path` rejects NUL before `os.replace` can leak a `ValueError`).

### Security

- **Hardening bundle.** Smaller defence-in-depth items from the audit (closes [#150](https://github.com/Digital-Process-Tools/claude-supertool/issues/150)):
  - **Gitcli flag smuggling**: `git-checkout REF`, `git-merge REF`, `git-blame PATH` reject refs/paths starting with `-` (e.g. `--orphan=evil`, `--abort`, `-X theirs`). `git-blame` uses `--` separator to keep a path-with-dash from being parsed as a flag.
  - **Regex ReDoS guards** on `grep`: pattern length capped at 1000 chars; nested unbounded quantifiers (`(a+)+`, `(.+)*`) rejected before they touch file content.
  - **`validate_staged` / `format_staged`** use `git diff -z --diff-filter=ACMR` (filenames with newlines/quotes survive) and reject symlinks — a staged symlink to `/etc/hosts` would otherwise be REWRITTEN by formatters.
  - **`xmllint` validator** runs with `--nonet --noent` so libxml2 can't fetch external entities or follow URI references during validation (XXE defence-in-depth).
  - **Validator cache HMAC** at `~/.cache/supertool/.cache_key` (mode 0600, 32 random bytes, generated on first use). Each cache entry wrapped with HMAC-SHA256 of its body. Attacker with write access to `~/.cache/supertool/validators/` cannot forge a passing `ok: true` entry without also reading the secret. Legacy unwrapped entries treated as miss.
- **Social publishing safety.** Three guards for the dev.to / bluesky publish + comment ops (closes [#149](https://github.com/Digital-Process-Tools/claude-supertool/issues/149)):
  - `file://` body must resolve under `.max/`, `drafts/`, `posts/`, or `blog/` (relative to cwd). Closes the credential-exfil vector: `bluesky_publish:file:///Users/.../app_password` now rejected before the file is read. Extend additively via `$SUPERTOOL_PUBLISH_BODY_ALLOWLIST=path1:path2` or `"publish_body_allowlist": [...]` in `.supertool.json`.
  - Confirm-default — `bluesky_publish` / `devto_publish` refuse to run without `|force`, `SUPERTOOL_NO_PUBLISH_CONFIRM=1` env, or `"no_publish_confirm": true` in `.supertool.json`. Blocks single-shot publish from a prompt-injected op.
  - Token-file mode check — `check_token_file_mode` refuses to load tokens at mode 0644+ (group/world-readable). Mirrors `ssh`'s SSH-key check.
- **MCP daemon UDS hardened.** Socket + pidfile + log moved from `/tmp/supertool-mcp-<hash>.{sock,pid,log,stderr}` (world-writable parent dir, predictable hash) to per-user runtime dir (`$XDG_RUNTIME_DIR/supertool/mcp/` on Linux, `~/Library/Caches/supertool/mcp/` on macOS, `~/.cache/supertool/mcp/` fallback). Dir created mode `0700`, ownership-checked at startup. Pidfile + log files open with `O_NOFOLLOW|O_CREAT|O_EXCL` so symlink-squatting attacks no longer overwrite victim files. Peer-uid check on every accept (defence in depth on top of parent-dir perms). Server names validated against `[A-Za-z0-9_-]{1,64}`. `os.umask(0o077)` reset in detach. Override via `$SUPERTOOL_RUNTIME_DIR`. Closes [#148](https://github.com/Digital-Process-Tools/claude-supertool/issues/148).
- **Vim shell verbs gated.** `:!cmd`, `:%!cmd`, `:N,M!cmd`, `:r !cmd` are disabled by default. A prompt-injected vim payload like `:!rm -rf ~` is rejected with a clean error. Editor verbs (i/a/o/d/s/etc.) are unaffected. Opt-in via `SUPERTOOL_ALLOW_VIM_SHELL=1` (env) **or** `"allow_vim_shell": true` in `.supertool.json`. Closes [#147](https://github.com/Digital-Process-Tools/claude-supertool/issues/147).
- **`:r FILE` honours cwd containment.** Vim's `:r FILE` (without `!`) now routes through `_safe_path` — reading `/etc/passwd` or `~/.ssh/id_rsa` into the buffer is rejected. Opt-out is the same `SUPERTOOL_ALLOW_OUTSIDE_CWD=1` as the rest of #146.
- **Path containment (`_safe_path`).** Every op path arg now resolves under cwd. A malicious `.supertool.json` or prompt-injected op like `paste:~/.ssh/authorized_keys:::pwned` or `read:/etc/passwd` is rejected with a clean error. Symlinks crossing the boundary are caught (realpath follows them). NUL bytes rejected early. Closes [#146](https://github.com/Digital-Process-Tools/claude-supertool/issues/146).
- **Opt-out**: set `SUPERTOOL_ALLOW_OUTSIDE_CWD=1` (env, one-off) **or** `"allow_outside_cwd": true` in `.supertool.json` (project-pinned, no env var dance). Env takes precedence.
- **Default excludes extended**: `.env`, `.env.*`, `.max/`, `.ssh/`, `.aws/`, `.gnupg/`, `.kube/`, `.docker/`, `.terraform/`, `.chef/`, `.npm/`, `secrets/`, `credentials/` are now pruned by `grep`/`glob`/`tree`/`map` so tokens don't surface into an LLM's context.
- Windows-compat: `os.path.normcase` handles case-insensitive drive letters (`c:\Users` == `C:\users`) and separator normalization.

## [0.13.1]

### Added

- Per-validator `cache: false` opt-out (`_validator_run_one`). Useful when the validator's input file isn't the only thing affecting results (e.g. phpunit: source + test + bootstrap + DI graph all matter, cache key only hashes the resolved file → stale hits when source changes).

### Changed

- `validators/rector-mcp/`: drops bare "would refactor X" entries that have no `applied_rectors` or diff. Rector returns these in `--debug` mode (which we need for speed on large configs) but they carry no actionable info. Real errors with rule names + diffs still pass through.

## [0.13.0]

### Added

- `validators/rector-mcp/` — bridges supertool validators to [`dpt/mcp-rector-warm`](https://github.com/Digital-Process-Tools/mcp-rector-warm) via UDS daemon. ~14× faster per edit on Rector validation.
- `validators/phpunit-mcp/` — bridges to [`dpt/mcp-phpunit-warm`](https://github.com/Digital-Process-Tools/mcp-phpunit-warm). ~25× faster per edit.
- `validators/phpstan-mcp/` — bridges to [`dpt/mcp-phpstan-warm`](https://github.com/Digital-Process-Tools/mcp-phpstan-warm) (PHPStan worker over TCP NDJSON). ~30× faster per edit.
- `docs/mcp-warm-process-servers.md` — overview of the three warm MCP servers and how to wire them.

All three adapters share the same shape: auto-spawn UDS daemon via `presets/mcp/daemon.py`, send MCP `initialize` + `tools/call`, format response as SCHEMA.md validator JSON.

## [0.11.0]

Initial public changelog. See git history for prior versions.

[Unreleased]: https://github.com/Digital-Process-Tools/claude-supertool/compare/v0.13.0...HEAD
[0.13.0]: https://github.com/Digital-Process-Tools/claude-supertool/releases/tag/v0.13.0
[0.11.0]: https://github.com/Digital-Process-Tools/claude-supertool/releases/tag/v0.11.0
