From 6c929c7c39fc635b921db5d521ce1fe884d5bffc Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Sun, 30 Aug 2026 18:12:33 -0400 Subject: [PATCH 1/2] fix(source-control): convert the mktemp path to Windows mixed form before it feeds the Write tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The worktree create context told the model to print a bare `mktemp -d` path and use it as a `Write` tool `file_path`. On Windows Git Bash that prints the MSYS literal `/tmp/tmp.XXXXXXXXXX`; the Write tool's native side resolves the leading `/` against the current drive, creating a phantom `:\tmp\...` while the real directory sits in %TEMP% — the same silent drive-root class as #2834. Confirmed live: an empty `C:\tmp\tmp.rSFIkHm5DO` created by exactly this path. Per docs/conventions/windows-path-emit/README.md Rule 3 (cygpath mixed form, correct for both the Write tool and the later Bash consumers of the same printed value; `-l` expands the 8.3 short name %TEMP% carries) and Rule 4 (fail loud — `|| exit 2`, never the unconverted literal). Non-Windows passes through unchanged, gated on $OSTYPE exactly as scripts/emit-windows-path.sh does; the helper itself is marketplace-root tooling the shipped plugin cannot call, so the contract is inlined. `mktemp -d -p "$TEMP"` was rejected: `mktemp -p` is an active flagged token in scripts/shell-portability-tokens.txt, and it yields mixed separators anyway. The load-bearing-details list now documents the conversion so it is not reverted as noise. Co-Authored-By: Claude Fable 5 --- plugins/source-control/skills/worktree/context/create.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/plugins/source-control/skills/worktree/context/create.md b/plugins/source-control/skills/worktree/context/create.md index 9c28a74aa..c4992dc68 100644 --- a/plugins/source-control/skills/worktree/context/create.md +++ b/plugins/source-control/skills/worktree/context/create.md @@ -85,7 +85,11 @@ Two steps — the helper creates and places the worktree; `EnterWorktree(path:)` Four steps: ```bash - root_dir="$(mktemp -d)"; printf '%s\n' "$root_dir" + root_dir="$(mktemp -d)" + case "${OSTYPE:-}" in + msys* | cygwin* | win32) root_dir="$(cygpath -m -l -- "$root_dir")" || exit 2 ;; + esac + printf '%s\n' "$root_dir" ``` `Write(file_path: "/worktree-root", content: "${user_config.worktree_root}")` — the substituted value is the entire `content`, written byte-exact with nothing appended (no trailing newline). @@ -101,9 +105,10 @@ Two steps — the helper creates and places the worktree; `EnterWorktree(path:)` exit "$status" ``` - Two details in that last block are load-bearing: + Three details in those blocks are load-bearing: - **`mktemp -d`, not `mktemp`** — `Write` refuses to overwrite a file it has not read, so the directory must exist and the file inside it must not. + - **The `cygpath -m -l` conversion on Windows** — the printed path crosses the Git Bash → native boundary: it becomes a `Write` tool `file_path`, and node's Win32 side resolves an MSYS literal like `/tmp/tmp.XXX` against the **current drive**, silently creating a phantom `:\tmp\...` while the real directory sits in `%TEMP%` ([the windows-path-emit convention](../../../../../docs/conventions/windows-path-emit/README.md), Rules 3–4). Mixed form (`-m`) is correct for **both** consumers — the `Write` tool and the later Bash block — so one converted value round-trips everywhere; `-l` expands an 8.3 short name (`KYLESE~1`) whose `~` misbehaves downstream. The `|| exit 2` is the fail-loud posture: never fall back to the unconverted literal, because the unconverted literal is exactly what writes to the wrong place. On non-Windows the `case` passes the path through unchanged. Do **not** replace this with `mktemp -d -p "$TEMP"`: `mktemp -p` is a flagged GNU/BSD-divergence token in the portability gate, and it yields mixed separators anyway. - **`status=$?` before the cleanup, `exit "$status"` after** — `rm` almost always succeeds, so leaving it last would make the whole invocation report 0 and hide a helper refusal (exit 3) behind a green result, which step 2's "on a non-zero exit, STOP" would then never see. The helper prints the created worktree path as its **sole stdout line**; capture it. Resolution is most-specific-first: `melodic.worktreeroot` (if set on the target repository) outranks the plugin option in `--fallback-root-file`. When `worktree_root` is unset, Claude leaves the literal `${user_config.worktree_root}` token — `Write` puts that token in the file verbatim, the helper reads it as "unconfigured", and the root resolves from the data-root file instead (`/worktrees`, announced on stderr, exit 0) unless the git config key already supplied one. Only when no rung yields a usable root does it refuse. A value carrying a newline byte anywhere — including a trailing one — is rejected loudly by the helper (exit 2); a path with a newline in it is malformed configuration, not a root to silently trim. From 9f4a700fb197626a3f91686e63f464b2caf43c30 Mon Sep 17 00:00:00 2001 From: Kyle Sexton <153232337+kyle-sexton@users.noreply.github.com> Date: Mon, 31 Aug 2026 01:05:44 -0400 Subject: [PATCH 2/2] =?UTF-8?q?chore(source-control):=20release=200.55.34?= =?UTF-8?q?=20=E2=80=94=20ship=20the=20create.md=20Windows=20path=20fix=20?= =?UTF-8?q?to=20existing=20installs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The plugin version is the consumer update-cache key: without a bump, installations already carrying an earlier version keep the old create.md and never receive the Windows mixed-form conversion, so the phantom drive-root write stays live on every existing install. Bump the manifest and add the matching '## [0.55.34]' release entry (changelog-parity --check-bump requires the pair; 0.55.31-0.55.33 are already published on main and may not be reused — originally staged as 0.55.32, renumbered on rebase after main released 0.55.32 and 0.55.33). Co-Authored-By: Claude Fable 5 --- plugins/source-control/.claude-plugin/plugin.json | 2 +- plugins/source-control/CHANGELOG.md | 15 +++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/plugins/source-control/.claude-plugin/plugin.json b/plugins/source-control/.claude-plugin/plugin.json index e4ac293ad..1bdb7782f 100644 --- a/plugins/source-control/.claude-plugin/plugin.json +++ b/plugins/source-control/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "source-control", - "version": "0.55.33", + "version": "0.55.34", "description": "Git and GitHub delivery workflow: /commit (Conventional Commits + Co-authored-by trailer via safe heredoc mechanics), /pull-request (prep, create, CI monitoring, review-comment triage, merge, CI-log fetch), /babysit-prs (self-pacing fleet loop \u2014 safe by default; opt-in worker/autopilot tiers add gate-checked merge and thread resolution behind a deterministic Python engine), /babysit-loop (the loop-lane merge lane: a standing or drain loop that invokes babysit-prs per cycle, configured through repo-scoped babysit_loop_* keys on the layered source-control.md seam, with merge authority human-only until the target repo's tracked config adopts the lane, a gate-proven C2-mechanical baseline once adopted, and standing merge-rung raises binding from the team-tracked layer only \u2014 with one named exception, where an invocation line explicitly typing both the autopilot tier keyword and the dedicated raise argument --merge c3-this-run widens that single invocation's merge authority up to C3 behind a fresh independent frontier-tier resolver, while C4-structural and C5-untrusted-provenance stay unconditionally human-merge), /worktree (create, status, cleanup, audit for parallel-session isolation), /setup (check the effective commit-subject / PR-title convention merged across its config layers and the babysit-prs config, or apply \u2014 interview the repo and write the convention config to a chosen layer), and /resolve-conflicts (intent-first merge/rebase conflict resolution with a semantic-conflict sweep \u2014 never --abort). The commit-subject / PR-title convention is configurable via a source-control.md config written by a re-runnable setup skill, layered across a ~/.claude user-global file, the tracked team file, and a gitignored .claude/source-control.local.md personal overlay merged per key; Conventional Commits is the default when no convention is declared.", "author": { "name": "Melodic Software", diff --git a/plugins/source-control/CHANGELOG.md b/plugins/source-control/CHANGELOG.md index 5958b1517..3d34e129b 100644 --- a/plugins/source-control/CHANGELOG.md +++ b/plugins/source-control/CHANGELOG.md @@ -3,6 +3,21 @@ All notable changes to the `source-control` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.55.34] + +### Fixed + +- **The worktree `create` snippet no longer emits a bare MSYS temp path on Windows.** The + `mktemp -d` step in `skills/worktree/context/create.md` printed the POSIX literal + `/tmp/tmp.XXXXXXXXXX`, which the native `Write` tool resolves against the current drive — + creating a phantom `:\tmp\...` while the real directory sits in `%TEMP%` (the silent + drive-root emit class the marketplace's windows-path-emit convention owns). The snippet now + converts at the boundary with `cygpath -m -l` (mixed form works for both the `Write` tool and + the later Bash consumers; `-l` expands an 8.3 short name), fails loud rather than falling back + to the unconverted literal, and passes through unchanged on non-Windows hosts. The + load-bearing-details list documents the conversion — including why `mktemp -d -p "$TEMP"` is + rejected — so it is not reverted as noise. + ## [0.55.33] ### Fixed