Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugins/toolchain/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "toolchain",
"version": "0.4.1",
"version": "0.4.2",
"description": "Repo-agnostic polyglot verification toolchain: build + test + lint for changed files across .NET, Python, TypeScript, Bash, PowerShell, Markdown, YAML, and cross-cutting surfaces (`/toolchain:check`, `/toolchain:lint`), plus a re-runnable `/toolchain:setup` with check (report the configured ecosystems and their command surface) and apply (interview, infer, and write the tracked per-ecosystem command config those skills resolve first).",
"author": {
"name": "Melodic Software",
Expand Down
30 changes: 30 additions & 0 deletions plugins/toolchain/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,36 @@
All notable changes to the `toolchain` plugin are documented here. Format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning.

## [0.4.2]

### Fixed

- **Remote selected from those present, not assumed `origin`.** The clean-working-tree branch-diff
fallback in `/toolchain:check` and `/toolchain:lint` resolved the remote from the current branch's
`branch.<name>.remote`, but when that was unset (an unpushed feature branch) it forced `REMOTE=origin`
unconditionally. In a clone made with a differently named remote (`git clone -o vendor`) that has no
`origin` and no pushed upstream, `origin` does not exist, so `git symbolic-ref refs/remotes/origin/HEAD`
and every subsequent probe failed and the branch diff was skipped ("branch diff unavailable") — the
`origin` fallback the 0.4.1 note claimed "still resolves" a `git clone -o vendor` did not hold for the
not-yet-pushed case. Both call sites now probe candidate remotes in priority order — the branch's
tracking remote, then `origin` if present, then the rest — and select the first whose default branch
resolves to a locally available `refs/remotes/<remote>/<branch>` tracking ref. This also skips a remote
that was added but never fetched (whose `git ls-remote` default-branch query succeeds over the network
but leaves no local ref for `git merge-base`) in favor of a later remote that has one, rather than
committing to the alphabetically first remote and bailing. The common tracking-remote case still
short-circuits on the first candidate with no extra network calls, and detection still degrades
gracefully (skips the branch-diff path) when no candidate yields a local default branch. Candidate
remote names are option-parse-safe: the tracking-ref checks use the fully-qualified
`refs/remotes/<remote>/<branch>` form and the `git ls-remote` probe passes `--end-of-options`, so a
Git-legal remote whose name begins with a dash (`git clone --origin=-x`) is not misparsed as a command
option and skipped. A cross-plugin shared default-branch helper remains the broader fix tracked by
#436/#442.
- **Remote-prefix strip no longer breaks on `#` in a remote name.** The default-branch resolution
stripped the `$REMOTE/` prefix with `sed "s#^$REMOTE/##"`, whose `#` delimiter collides with a
`#` in the remote name (a Git-legal character), corrupting `DEFAULT_BRANCH` and silently skipping
the branch diff. Both call sites now strip the prefix with the `${DEFAULT_BRANCH#"$REMOTE/"}`
parameter expansion, which treats the remote name literally regardless of its characters.

## [0.4.1]

### Fixed
Expand Down
23 changes: 16 additions & 7 deletions plugins/toolchain/skills/check/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,18 +57,27 @@ If `$ARGUMENTS` specifies an ecosystem, use it. If `all`, run every covered ecos
If the working tree is clean, fall back to the branch diff so checkpoint-committed work still gets classified (the common pre-PR case: every green block was already committed). Resolve the default branch by **detection, not assumption** — never a hardcoded `main`/`master` — and assign it before use:

```bash
Comment thread
kyle-sexton marked this conversation as resolved.
REMOTE=$(git config "branch.$(git branch --show-current | tr -d '\r').remote" 2>/dev/null | tr -d '\r')
[[ -z "$REMOTE" || "$REMOTE" == "." ]] && REMOTE=origin
DEFAULT_BRANCH=$(git symbolic-ref --short "refs/remotes/$REMOTE/HEAD" 2>/dev/null | sed "s#^$REMOTE/##")
DEFAULT_BRANCH=${DEFAULT_BRANCH:-$(git ls-remote --symref "$REMOTE" HEAD 2>/dev/null | awk '/^ref:/{sub(/refs\/heads\//,"",$2); print $2; exit}')}
if [[ -n "$DEFAULT_BRANCH" ]] && git rev-parse --verify --quiet "$REMOTE/$DEFAULT_BRANCH" >/dev/null; then
git diff --name-only "$(git merge-base "$REMOTE/$DEFAULT_BRANCH" HEAD)..HEAD"
REMOTE="" DEFAULT_BRANCH=""
TRACKED=$(git config "branch.$(git branch --show-current | tr -d '\r').remote" 2>/dev/null | tr -d '\r')
[[ "$TRACKED" == "." ]] && TRACKED=""
CANDIDATES=$( { [[ -n "$TRACKED" ]] && echo "$TRACKED"; git remote | grep -qx origin && echo origin; git remote; } | awk 'NF && !seen[$0]++' )
while IFS= read -r CANDIDATE; do
BRANCH=$(git symbolic-ref --short "refs/remotes/$CANDIDATE/HEAD" 2>/dev/null)
BRANCH=${BRANCH#"$CANDIDATE/"}
BRANCH=${BRANCH:-$(git ls-remote --symref --end-of-options "$CANDIDATE" HEAD 2>/dev/null | awk '/^ref:/{sub(/refs\/heads\//,"",$2); print $2; exit}')}
if [[ -n "$BRANCH" ]] && git rev-parse --verify --quiet "refs/remotes/$CANDIDATE/$BRANCH" >/dev/null; then
REMOTE=$CANDIDATE DEFAULT_BRANCH=$BRANCH
break
fi
done <<< "$CANDIDATES"
if [[ -n "$REMOTE" ]]; then
git diff --name-only "$(git merge-base "refs/remotes/$REMOTE/$DEFAULT_BRANCH" HEAD)..HEAD"
else
echo "branch diff unavailable (could not detect default branch)"
fi
```

`$REMOTE` is the remote the current branch tracks (`branch.<name>.remote`), falling back to `origin` — never a hardcoded remote name, so a repo cloned with a different remote name (e.g. `git clone -o vendor`) still resolves. The fallback queries that remote's own `HEAD` (not the current branch's upstream, which on a pushed feature branch points at the feature branch itself and would make `merge-base` equal `HEAD`, yielding an empty diff). `merge-base` is taken against the remote-tracking ref `$REMOTE/$DEFAULT_BRANCH`, which resolves without a local branch of that name. If detection yields no default branch (no `$REMOTE/HEAD`, and the remote query fails or its ref is absent locally), skip the branch-diff path rather than guessing. A caller passing an explicit changed-file list (e.g. `/verification:confirm`) overrides both detection paths.
The loop probes candidate remotes in priority order — the remote the current branch tracks (`branch.<name>.remote`) first, then `origin` if present, then the rest — and selects the first one whose default branch resolves to a locally available tracking ref, never a hardcoded remote name. This handles an unpushed feature branch (no tracking remote) in a repo cloned with a different remote name (e.g. `git clone -o vendor`), and skips a remote that was added but never fetched (its default branch has no local `refs/remotes/<remote>/<branch>` to diff against) in favor of a later remote that does — the candidate is accepted only when `git rev-parse` confirms the tracking ref exists locally. Each candidate's default branch comes from that remote's own `HEAD` (not the current branch's upstream, which on a pushed feature branch points at the feature branch itself and would make `merge-base` equal `HEAD`, yielding an empty diff), falling back to a `git ls-remote --symref` query when the local `HEAD` symref is absent. `merge-base` is taken against the fully-qualified remote-tracking ref `refs/remotes/$REMOTE/$DEFAULT_BRANCH`, which resolves without a local branch of that name and — like the `rev-parse` verify — cannot be misparsed as an option when the remote name begins with a dash (`git clone --origin=-x`); the `git ls-remote` probe terminates option parsing with `--end-of-options` for the same reason. If no candidate yields a locally available default branch, skip the branch-diff path rather than guessing. A caller passing an explicit changed-file list (e.g. `/verification:confirm`) overrides both detection paths.

If neither path yields changes and no `$ARGUMENTS`: report "No changes found (working tree clean, no branch diff vs the default branch). Use `/toolchain:check all` to verify the full repo, or `/toolchain:check <ecosystem>` for a specific ecosystem." and exit.

Expand Down
23 changes: 16 additions & 7 deletions plugins/toolchain/skills/lint/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,18 +59,27 @@ Parse `$ARGUMENTS` for:
If an ecosystem filter was provided, use it. If `all`, run every applicable ecosystem from the config. Otherwise, classify changed files from `git status --porcelain` against each ecosystem's `globs` list; when the working tree is clean, fall back to the branch diff so checkpoint-committed work still gets classified. Resolve the default branch by **detection, not assumption** — never a hardcoded `main`/`master` — and assign it before use:

```bash
REMOTE=$(git config "branch.$(git branch --show-current | tr -d '\r').remote" 2>/dev/null | tr -d '\r')
[[ -z "$REMOTE" || "$REMOTE" == "." ]] && REMOTE=origin
DEFAULT_BRANCH=$(git symbolic-ref --short "refs/remotes/$REMOTE/HEAD" 2>/dev/null | sed "s#^$REMOTE/##")
DEFAULT_BRANCH=${DEFAULT_BRANCH:-$(git ls-remote --symref "$REMOTE" HEAD 2>/dev/null | awk '/^ref:/{sub(/refs\/heads\//,"",$2); print $2; exit}')}
if [[ -n "$DEFAULT_BRANCH" ]] && git rev-parse --verify --quiet "$REMOTE/$DEFAULT_BRANCH" >/dev/null; then
git diff --name-only "$(git merge-base "$REMOTE/$DEFAULT_BRANCH" HEAD)..HEAD"
REMOTE="" DEFAULT_BRANCH=""
TRACKED=$(git config "branch.$(git branch --show-current | tr -d '\r').remote" 2>/dev/null | tr -d '\r')
[[ "$TRACKED" == "." ]] && TRACKED=""
CANDIDATES=$( { [[ -n "$TRACKED" ]] && echo "$TRACKED"; git remote | grep -qx origin && echo origin; git remote; } | awk 'NF && !seen[$0]++' )
while IFS= read -r CANDIDATE; do
BRANCH=$(git symbolic-ref --short "refs/remotes/$CANDIDATE/HEAD" 2>/dev/null)
BRANCH=${BRANCH#"$CANDIDATE/"}
BRANCH=${BRANCH:-$(git ls-remote --symref --end-of-options "$CANDIDATE" HEAD 2>/dev/null | awk '/^ref:/{sub(/refs\/heads\//,"",$2); print $2; exit}')}
if [[ -n "$BRANCH" ]] && git rev-parse --verify --quiet "refs/remotes/$CANDIDATE/$BRANCH" >/dev/null; then
REMOTE=$CANDIDATE DEFAULT_BRANCH=$BRANCH
break
fi
done <<< "$CANDIDATES"
if [[ -n "$REMOTE" ]]; then
git diff --name-only "$(git merge-base "refs/remotes/$REMOTE/$DEFAULT_BRANCH" HEAD)..HEAD"
else
echo "branch diff unavailable (could not detect default branch)"
fi
```

`$REMOTE` is the remote the current branch tracks (`branch.<name>.remote`), falling back to `origin` — never a hardcoded remote name, so a repo cloned with a different remote name (e.g. `git clone -o vendor`) still resolves. The fallback queries that remote's own `HEAD` (not the current branch's upstream, which on a pushed feature branch points at the feature branch itself and would make `merge-base` equal `HEAD`, yielding an empty diff). `merge-base` is taken against the remote-tracking ref `$REMOTE/$DEFAULT_BRANCH`, which resolves without a local branch of that name. If detection yields no default branch (no `$REMOTE/HEAD`, and the remote query fails or its ref is absent locally), skip the branch-diff path rather than guessing. A caller passing an explicit changed-file list (e.g. `/verification:confirm`) overrides both detection paths. Cross-cutting runs alongside detected ecosystems when ANY text file changed AND the repo opts into its tools.
The loop probes candidate remotes in priority order — the remote the current branch tracks (`branch.<name>.remote`) first, then `origin` if present, then the rest — and selects the first one whose default branch resolves to a locally available tracking ref, never a hardcoded remote name. This handles an unpushed feature branch (no tracking remote) in a repo cloned with a different remote name (e.g. `git clone -o vendor`), and skips a remote that was added but never fetched (its default branch has no local `refs/remotes/<remote>/<branch>` to diff against) in favor of a later remote that does — the candidate is accepted only when `git rev-parse` confirms the tracking ref exists locally. Each candidate's default branch comes from that remote's own `HEAD` (not the current branch's upstream, which on a pushed feature branch points at the feature branch itself and would make `merge-base` equal `HEAD`, yielding an empty diff), falling back to a `git ls-remote --symref` query when the local `HEAD` symref is absent. `merge-base` is taken against the fully-qualified remote-tracking ref `refs/remotes/$REMOTE/$DEFAULT_BRANCH`, which resolves without a local branch of that name and — like the `rev-parse` verify — cannot be misparsed as an option when the remote name begins with a dash (`git clone --origin=-x`); the `git ls-remote` probe terminates option parsing with `--end-of-options` for the same reason. If no candidate yields a locally available default branch, skip the branch-diff path rather than guessing. A caller passing an explicit changed-file list (e.g. `/verification:confirm`) overrides both detection paths. Cross-cutting runs alongside detected ecosystems when ANY text file changed AND the repo opts into its tools.

Auto-detection algorithm:

Expand Down
Loading