mirror of
https://github.com/max-sixty/worktrunk.git
synced 2026-09-14 20:00:38 +08:00
worktree-path-not-found-error
4850 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
80dadb176e | Merge branch 'main' into worktree-path-not-found-error | ||
|
|
1636b78ddf | fix(gitlab): forward glab's verdict when the project lookup fails (#3799) | ||
|
|
b688e8142b | test(docs): drop the command-page sync test its docs file outlived (#3802) | ||
|
|
37562f5cfa | Merge branch 'main' into worktree-path-not-found-error | ||
|
|
91b7beff57 |
skills(writing-user-outputs): document the stderr half of the anstream macro rule (#3797)
Skill drift. `writing-user-outputs` is the thing loaded before editing
anything that produces user-visible strings, and its "Printing output"
section states only the stdout half of a two-sided rule — which
`println!` is in scope, and the `BrokenPipe` panic that turns on it. The
stderr half is absent, even though
|
||
|
|
e243472d9a |
docs(remove): correct the detached-worktree case in the removal planner (#3796)
Comment-only. Two adjacent comments in `prepare_worktree_removal`'s
`WorktreePath` arm describe a routing that
|
||
|
|
6466b91673 |
chore: bump the patch group with 6 updates (#3794)
Bumps the patch group with 6 updates: | Package | From | To | | --- | --- | --- | | [clap_complete](https://github.com/clap-rs/clap) | `4.6.8` | `4.6.9` | | [ignore](https://github.com/BurntSushi/ripgrep) | `0.4.31` | `0.4.33` | | [open](https://github.com/Byron/open-rs) | `5.4.0` | `5.4.1` | | [similar](https://github.com/mitsuhiko/similar) | `3.1.1` | `3.1.2` | | [jsonschema](https://github.com/Stranger6667/jsonschema) | `0.49.5` | `0.49.6` | | [vergen-gitcl](https://github.com/rustyhorde/vergen) | `10.0.1` | `10.0.2` | Updates `clap_complete` from 4.6.8 to 4.6.9 <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/clap-rs/clap/commit/715b812f3b72ebca1d5ce257e0495e1e76aea56f"><code>715b812</code></a> chore: Release</li> <li><a href="https://github.com/clap-rs/clap/commit/89e6e9b59c6209c5d9817c7d113ad7ad7f6d6907"><code>89e6e9b</code></a> docs: Update changelog</li> <li><a href="https://github.com/clap-rs/clap/commit/3918cb4db643ffdfb3c8206d241d70acf6d5c5de"><code>3918cb4</code></a> Merge pull request <a href="https://redirect.github.com/clap-rs/clap/issues/6468">#6468</a> from sjh9714/fix-bash-posix-fn-name</li> <li><a href="https://github.com/clap-rs/clap/commit/526c81906357cae595259d46cea2fd4675489331"><code>526c819</code></a> Merge pull request <a href="https://redirect.github.com/clap-rs/clap/issues/6469">#6469</a> from latent-9/docs/fix-derive-reference-link</li> <li><a href="https://github.com/clap-rs/clap/commit/b2bd685a8e809df2049f5cf6777641d8f86aeef6"><code>b2bd685</code></a> docs(clap_derive): Fix derive reference link</li> <li><a href="https://github.com/clap-rs/clap/commit/ecee8a4f862b1b97ce2c7db5bf806cfe394a55f6"><code>ecee8a4</code></a> fix(complete): Name the function after fn_name</li> <li><a href="https://github.com/clap-rs/clap/commit/6dd2e5a0802d84226afd63c427fc09940fa837d4"><code>6dd2e5a</code></a> test(complete): Show POSIX bash will not source</li> <li><a href="https://github.com/clap-rs/clap/commit/4684d7abc545cef1d78708864cfe8c7668ed49c1"><code>4684d7a</code></a> chore: Release</li> <li><a href="https://github.com/clap-rs/clap/commit/51d50ad3ae341f987f26115d6a6719cf76cc78f4"><code>51d50ad</code></a> docs: Update changelog</li> <li><a href="https://github.com/clap-rs/clap/commit/3bc36222053a49936ea67e8616d1584a75c811b0"><code>3bc3622</code></a> Merge pull request <a href="https://redirect.github.com/clap-rs/clap/issues/6457">#6457</a> from bl4ck4t/master</li> <li>Additional commits viewable in <a href="https://github.com/clap-rs/clap/compare/clap_complete-v4.6.8...clap_complete-v4.6.9">compare view</a></li> </ul> </details> <br /> Updates `ignore` from 0.4.31 to 0.4.33 <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/BurntSushi/ripgrep/commit/3fce3b5bb0236da2df6d99672afb8a719642eca7"><code>3fce3b5</code></a> ignore-0.4.33</li> <li><a href="https://github.com/BurntSushi/ripgrep/commit/5055264ae021b5d0e01e55f723622d638ea877f0"><code>5055264</code></a> globset-0.4.20</li> <li><a href="https://github.com/BurntSushi/ripgrep/commit/020687a77d13146923333f0beb274eeabd54a270"><code>020687a</code></a> ignore,globset: increase pool capacity</li> <li><a href="https://github.com/BurntSushi/ripgrep/commit/5ed408e17eccfc59bcec48f584feece902873e54"><code>5ed408e</code></a> ignore-0.4.32</li> <li><a href="https://github.com/BurntSushi/ripgrep/commit/435f59fc4b43af3ab32f34d53fa34978f393fe52"><code>435f59f</code></a> ignore: skip loading unreachable ignore files</li> <li><a href="https://github.com/BurntSushi/ripgrep/commit/f9c05a949d1a0dc8e16dee28ca9605d38611faeb"><code>f9c05a9</code></a> index: remove incorrect README</li> <li><a href="https://github.com/BurntSushi/ripgrep/commit/8372866810a1f2a647d11d7780984d4402a5c1e9"><code>8372866</code></a> index: add some initial indexing scaffolding</li> <li><a href="https://github.com/BurntSushi/ripgrep/commit/d99ac34406f82c03c3938c222f1786495c845034"><code>d99ac34</code></a> core: add <code>index</code> module</li> <li><a href="https://github.com/BurntSushi/ripgrep/commit/2ed0c006fee4c441add774a711d9122a88477619"><code>2ed0c00</code></a> flags: disable many flags when indexing is enabled</li> <li>See full diff in <a href="https://github.com/BurntSushi/ripgrep/compare/ignore-0.4.31...ignore-0.4.33">compare view</a></li> </ul> </details> <br /> Updates `open` from 5.4.0 to 5.4.1 <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/Byron/open-rs/releases">open's releases</a>.</em></p> <blockquote> <h2>v5.4.1</h2> <h3>Bug Fixes</h3> <ul> <li> <p>Forward WSL targets to PowerShell to make <code>open</code> actually work there</p> <!-- raw HTML omitted --> <p>Opening a URL from WSL failed because the Windows process did not receive OPEN_RS_TARGET, even though it was present in the Linux command environment. The failure reproduces with cargo run -- <a href="https://google.com">https://google.com</a> on Ubuntu under WSL, where Start-Process receives a null FilePath and exits unsuccessfully.</p> <p>Add OPEN_RS_TARGET to WSLENV so WSL interop forwards the target into the PowerShell environment. Preserve existing WSLENV entries and their flags, while keeping the PowerShell command fixed so targets remain data rather than shell code.</p> <p>Validated with focused WSL tests, the all-features test suite, rustfmt, Clippy with warnings denied, and an end-to-end cargo run from WSL.</p> </li> </ul> <h3>Commit Statistics</h3> <ul> <li>1 commit contributed to the release.</li> <li>24 days passed between releases.</li> <li>1 commit was understood as <a href="https://www.conventionalcommits.org">conventional</a>.</li> <li>1 unique issue was worked on: <a href="https://redirect.github.com/Byron/open-rs/issues/128">#128</a></li> </ul> <h3>Commit Details</h3> <!-- raw HTML omitted --> <!-- raw HTML omitted --> <ul> <li><strong><a href="https://redirect.github.com/Byron/open-rs/issues/128">#128</a></strong> <ul> <li>Forward WSL targets to PowerShell to make <code>open</code> actually work there (96fa673)</li> </ul> </li> </ul> <!-- raw HTML omitted --> </blockquote> </details> <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/Byron/open-rs/blob/main/changelog.md">open's changelog</a>.</em></p> <blockquote> <h2>5.4.1 (2026-08-05)</h2> <h3>Bug Fixes</h3> <ul> <li> <p><!-- raw HTML omitted --> Forward WSL targets to PowerShell to make <code>open</code> actually work there</p> <!-- raw HTML omitted --> <p>Opening a URL from WSL failed because the Windows process did not receive OPEN_RS_TARGET, even though it was present in the Linux command environment. The failure reproduces with cargo run -- <a href="https://google.com">https://google.com</a> on Ubuntu under WSL, where Start-Process receives a null FilePath and exits unsuccessfully.</p> <p>Add OPEN_RS_TARGET to WSLENV so WSL interop forwards the target into the PowerShell environment. Preserve existing WSLENV entries and their flags, while keeping the PowerShell command fixed so targets remain data rather than shell code.</p> <p>Validated with focused WSL tests, the all-features test suite, rustfmt, Clippy with warnings denied, and an end-to-end cargo run from WSL.</p> </li> </ul> <h3>Commit Statistics</h3> <!-- raw HTML omitted --> <ul> <li>1 commit contributed to the release.</li> <li>24 days passed between releases.</li> <li>1 commit was understood as <a href="https://www.conventionalcommits.org">conventional</a>.</li> <li>1 unique issue was worked on: <a href="https://redirect.github.com/Byron/open-rs/issues/128">#128</a></li> </ul> <h3>Commit Details</h3> <!-- raw HTML omitted --> <!-- raw HTML omitted --> <ul> <li><strong><a href="https://redirect.github.com/Byron/open-rs/issues/128">#128</a></strong> <ul> <li>Forward WSL targets to PowerShell to make <code>open</code> actually work there (<a href="https://github.com/Byron/open-rs/commit/96fa673a6a152d193c210ff77766270192b42d16"><code>96fa673</code></a>)</li> </ul> </li> </ul> <!-- raw HTML omitted --> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/Byron/open-rs/commit/e548a4e9180ef8ca7e6b605f3f248fee534a03d1"><code>e548a4e</code></a> Release open v5.4.1</li> <li><a href="https://github.com/Byron/open-rs/commit/96fa673a6a152d193c210ff77766270192b42d16"><code>96fa673</code></a> fix: Forward WSL targets to PowerShell to make <code>open</code> actually work there (<a href="https://redirect.github.com/Byron/open-rs/issues/128">#128</a>)</li> <li>See full diff in <a href="https://github.com/Byron/open-rs/compare/v5.4.0...v5.4.1">compare view</a></li> </ul> </details> <br /> Updates `similar` from 3.1.1 to 3.1.2 <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/mitsuhiko/similar/blob/main/CHANGELOG.md">similar's changelog</a>.</em></p> <blockquote> <h2>3.1.2</h2> <ul> <li>Fixed <code>Algorithm::Lcs</code> deadline fallback emitting edits twice, which produced out-of-bounds <code>DiffOp</code> ranges and panics when consuming timed-out diffs. <a href="https://redirect.github.com/mitsuhiko/similar/issues/97">#97</a></li> <li>Fixed <code>Algorithm::Lcs</code> reporting identical subranges with zero-based indices instead of preserving the supplied range offsets. <a href="https://redirect.github.com/mitsuhiko/similar/issues/98">#98</a></li> <li>Fixed <code>Algorithm::Lcs</code> and <code>Algorithm::Hunt</code> emitting zero-length delete operations when both inputs are empty. <a href="https://redirect.github.com/mitsuhiko/similar/issues/99">#99</a></li> </ul> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/mitsuhiko/similar/commit/71f0a30e449f857aec934b839cc4c9ca5b21eb26"><code>71f0a30</code></a> chore(release): prepare 3.1.2</li> <li><a href="https://github.com/mitsuhiko/similar/commit/a0aa51e37a31a15898371430a6c03386b111303d"><code>a0aa51e</code></a> docs(changelog): document recent fixes</li> <li><a href="https://github.com/mitsuhiko/similar/commit/51195348b1c7f0bf46d79970294ee84a802e1500"><code>5119534</code></a> fix(algorithms): omit operations for empty inputs</li> <li><a href="https://github.com/mitsuhiko/similar/commit/043c9071ecdd328c9a6e8717f8765dda0c3982eb"><code>043c907</code></a> fix(lcs): preserve indices for equal subranges</li> <li><a href="https://github.com/mitsuhiko/similar/commit/cfad604ea2f213c6816095edb94e49c81768a1d8"><code>cfad604</code></a> fix(lcs): avoid duplicate deadline fallback edits</li> <li>See full diff in <a href="https://github.com/mitsuhiko/similar/compare/3.1.1...3.1.2">compare view</a></li> </ul> </details> <br /> Updates `jsonschema` from 0.49.5 to 0.49.6 <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/Stranger6667/jsonschema/releases">jsonschema's releases</a>.</em></p> <blockquote> <h2>[Python] Release 0.49.6</h2> <h3>Added</h3> <ul> <li>Canonicalization of a negated <code>uniqueItems</code>, where the complement demands a repeated element under the length floor two elements imply.</li> <li>Canonicalization of a negated array tuple, where each position's complement stands under the length that reaches it.</li> <li>Canonicalization of <code>contains</code> demands sharing no value, where their counts add up into a length floor.</li> <li><code>CanonicalSchema.negate</code> through references, where the complement of the resolved target takes the reference's place.</li> <li>Canonicalization of negated <code>propertyNames</code> and <code>additionalProperties</code>, where the complement spells the violating-key demand instead of a <code>not</code> residual.</li> <li>Canonicalization of negated <code>additionalProperties</code> under Draft 4, where the violating-key demand spells the closed property map.</li> <li>Canonicalization of <code>not</code> a reference, where the complement of the resolved target takes the pointer's place.</li> <li>Canonicalization of a negated <code>oneOf</code>, where the complement spells the values no branch admits beside the values two branches share.</li> <li>Canonicalization of negated <code>items</code> under Draft 4, where the violating-element demand spells the barred element schema.</li> </ul> <h3>Changed</h3> <ul> <li><code>ArrayView</code> reports distinctness in three states, so an array demanding a repeated element reads apart from one demanding distinct elements.</li> </ul> <h3>Fixed</h3> <ul> <li><code>CanonicalSchema.negate</code> keeping a root self-reference in the complement, where it points at the complement instead of the source.</li> <li>An <code>additionalItems</code> that tails no tuple keeping the whole document unmodeled under Draft 2020-12.</li> </ul> <h2>[Ruby] Release 0.49.6</h2> <h3>Added</h3> <ul> <li>Canonicalization of a negated <code>uniqueItems</code>, where the complement demands a repeated element under the length floor two elements imply.</li> <li>Canonicalization of a negated array tuple, where each position's complement stands under the length that reaches it.</li> <li>Canonicalization of <code>contains</code> demands sharing no value, where their counts add up into a length floor.</li> <li><code>CanonicalSchema#negate</code> through references, where the complement of the resolved target takes the reference's place.</li> <li>Canonicalization of negated <code>propertyNames</code> and <code>additionalProperties</code>, where the complement spells the violating-key demand instead of a <code>not</code> residual.</li> <li>Canonicalization of negated <code>additionalProperties</code> under Draft 4, where the violating-key demand spells the closed property map.</li> <li>Canonicalization of <code>not</code> a reference, where the complement of the resolved target takes the pointer's place.</li> <li>Canonicalization of a negated <code>oneOf</code>, where the complement spells the values no branch admits beside the values two branches share.</li> <li>Canonicalization of negated <code>items</code> under Draft 4, where the violating-element demand spells the barred element schema.</li> </ul> <h3>Changed</h3> <ul> <li><code>ArrayView</code> reports distinctness in three states, so an array demanding a repeated element reads apart from one demanding distinct elements.</li> </ul> <h3>Fixed</h3> <ul> <li><code>CanonicalSchema#negate</code> keeping a root self-reference in the complement, where it points at the complement instead of the source.</li> <li>An <code>additionalItems</code> that tails no tuple keeping the whole document unmodeled under Draft 2020-12.</li> </ul> <h2>[Rust] Release 0.49.6</h2> <h3>Added</h3> <ul> <li>Canonicalization of a negated <code>uniqueItems</code>, where the complement demands a repeated element under the length floor two elements imply.</li> <li>Canonicalization of a negated array tuple, where each position's complement stands under the length that reaches it.</li> <li>Canonicalization of <code>contains</code> demands sharing no value, where their counts add up into a length floor.</li> </ul> <!-- raw HTML omitted --> </blockquote> <p>... (truncated)</p> </details> <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/Stranger6667/jsonschema/blob/master/CHANGELOG.md">jsonschema's changelog</a>.</em></p> <blockquote> <h2>[0.49.6] - 2026-08-06</h2> <h3>Added</h3> <ul> <li>Canonicalization of a negated <code>uniqueItems</code>, where the complement demands a repeated element under the length floor two elements imply.</li> <li>Canonicalization of a negated array tuple, where each position's complement stands under the length that reaches it.</li> <li>Canonicalization of <code>contains</code> demands sharing no value, where their counts add up into a length floor.</li> <li><code>CanonicalSchema::negate</code> through references, where the complement of the resolved target takes the reference's place.</li> <li>Canonicalization of negated <code>propertyNames</code> and <code>additionalProperties</code>, where the complement spells the violating-key demand instead of a <code>not</code> residual.</li> <li>Canonicalization of negated <code>additionalProperties</code> under Draft 4, where the violating-key demand spells the closed property map.</li> <li>Canonicalization of <code>not</code> a reference, where the complement of the resolved target takes the pointer's place.</li> <li>Canonicalization of a negated <code>oneOf</code>, where the complement spells the values no branch admits beside the values two branches share.</li> <li>Canonicalization of negated <code>items</code> under Draft 4, where the violating-element demand spells the barred element schema.</li> </ul> <h3>Changed</h3> <ul> <li><code>ArrayView</code> reports distinctness in three states, so an array demanding a repeated element reads apart from one demanding distinct elements.</li> </ul> <h3>Fixed</h3> <ul> <li><code>CanonicalSchema::negate</code> keeping a root self-reference in the complement, where it points at the complement instead of the source.</li> <li>An <code>additionalItems</code> that tails no tuple keeping the whole document unmodeled under Draft 2020-12.</li> </ul> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/Stranger6667/jsonschema/commit/7888cb4bdc40fa07dd908a06802323d0f4ad134f"><code>7888cb4</code></a> chore(ruby): Release 0.49.6</li> <li><a href="https://github.com/Stranger6667/jsonschema/commit/d57a7e0b176923ebc9e6b05f39b5a6ca66709420"><code>d57a7e0</code></a> chore(python): Release 0.49.6</li> <li><a href="https://github.com/Stranger6667/jsonschema/commit/eaeb5b3ece16ec7cabca85bdf98c97601bcc1983"><code>eaeb5b3</code></a> chore(rust): Release 0.49.6</li> <li><a href="https://github.com/Stranger6667/jsonschema/commit/d87ab9b5f95d1bd73f9147a339b0f856c12150bc"><code>d87ab9b</code></a> fix: An <code>additionalItems</code> that tails no tuple keeping the whole document unmo...</li> <li><a href="https://github.com/Stranger6667/jsonschema/commit/b31c40474d01f83881479f20806b8254b4250311"><code>b31c404</code></a> feat: Canonicalization of a negated <code>uniqueItems</code>, where the complement deman...</li> <li><a href="https://github.com/Stranger6667/jsonschema/commit/cbcf7553ded29cc7e1e47acd4637a8b7069c5318"><code>cbcf755</code></a> build(deps): bump crates/jsonschema/tests/suite</li> <li><a href="https://github.com/Stranger6667/jsonschema/commit/d89a297b61a70cf040ac81febab580bffa8caea1"><code>d89a297</code></a> build(deps): bump crate-ci/typos from 1.48.0 to 1.49.0</li> <li><a href="https://github.com/Stranger6667/jsonschema/commit/8285c9619cc2294f2cea3473bb93fc014a20d563"><code>8285c96</code></a> feat: Canonicalization of a negated array tuple, where each position's comple...</li> <li><a href="https://github.com/Stranger6667/jsonschema/commit/05d579c9ec40f1e2e5c2c84a29d484a48a40a567"><code>05d579c</code></a> feat: Canonicalization of negated <code>items</code> under Draft 4, where the violating-...</li> <li><a href="https://github.com/Stranger6667/jsonschema/commit/8c53ddb0f4adab9db1ca6a49b7de61546b29989a"><code>8c53ddb</code></a> feat: Canonicalization of a negated <code>oneOf</code>, where the complement spells the ...</li> <li>Additional commits viewable in <a href="https://github.com/Stranger6667/jsonschema/compare/ruby-v0.49.5...ruby-v0.49.6">compare view</a></li> </ul> </details> <br /> Updates `vergen-gitcl` from 10.0.1 to 10.0.2 <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/rustyhorde/vergen/commit/8355721c8288a4853cb2f2d029ff8f217905c469"><code>8355721</code></a> chore(deps): dependency updates and Windows lcov fix (<a href="https://redirect.github.com/rustyhorde/vergen/issues/520">#520</a>)</li> <li><a href="https://github.com/rustyhorde/vergen/commit/ff70eebb23a0be1e15a23d07b5f91dc31572200c"><code>ff70eeb</code></a> chore(msrv): bump MSRV to 1.96.0 (<a href="https://redirect.github.com/rustyhorde/vergen/issues/519">#519</a>)</li> <li><a href="https://github.com/rustyhorde/vergen/commit/13e7cb6ae74143aa130f1dd12e89deb0d31dc353"><code>13e7cb6</code></a> chore(rake): Rakefile.toml build pipeline and lint updates (<a href="https://redirect.github.com/rustyhorde/vergen/issues/518">#518</a>)</li> <li><a href="https://github.com/rustyhorde/vergen/commit/a39ba617e1232a4da7381575558446dec8de5c69"><code>a39ba61</code></a> chore: replace deprecated provenance lints with implicit_provenance_casts (<a href="https://redirect.github.com/rustyhorde/vergen/issues/517">#517</a>)</li> <li><a href="https://github.com/rustyhorde/vergen/commit/8d9813a74034ca9382371e98ed3b0cd5125ac14b"><code>8d9813a</code></a> chore(rake): tweak macos daily (<a href="https://redirect.github.com/rustyhorde/vergen/issues/516">#516</a>)</li> <li><a href="https://github.com/rustyhorde/vergen/commit/cd4af2aaecb5d76c42e79f2b793f9a319169ec50"><code>cd4af2a</code></a> chore: add Rakefile.toml build pipeline and fix doc references (<a href="https://redirect.github.com/rustyhorde/vergen/issues/515">#515</a>)</li> <li>See full diff in <a href="https://github.com/rustyhorde/vergen/compare/10.0.1...v10.0.2">compare view</a></li> </ul> </details> <br /> Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore <dependency name> major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself) - `@dependabot ignore <dependency name> minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself) - `@dependabot ignore <dependency name>` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself) - `@dependabot unignore <dependency name>` will remove all of the ignore conditions of the specified dependency - `@dependabot unignore <dependency name> <ignore condition>` will remove the ignore condition of the specified dependency and ignore conditions </details> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
ec2aa4d154 |
chore: bump taiki-e/install-action from 2.85.8 to 2.85.10 (#3793)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.85.8 to 2.85.10. <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/taiki-e/install-action/releases">taiki-e/install-action's releases</a>.</em></p> <blockquote> <h2>2.85.10</h2> <ul> <li> <p>Update <code>uv@latest</code> to 0.12.2.</p> </li> <li> <p>Update <code>tombi@latest</code> to 1.2.7.</p> </li> <li> <p>Update <code>cosign@latest</code> to 3.1.3.</p> </li> <li> <p>Update <code>coreutils@latest</code> to 0.10.0.</p> </li> <li> <p>Update <code>cargo-rdme@latest</code> to 2.2.0.</p> </li> <li> <p>Update <code>cargo-crap@latest</code> to 0.4.3.</p> </li> </ul> <h2>2.85.9</h2> <ul> <li> <p>Update <code>zola@latest</code> to 0.23.1.</p> </li> <li> <p>Update <code>wild@latest</code> to 0.10.0.</p> </li> <li> <p>Update <code>mise@latest</code> to 2026.8.2.</p> </li> <li> <p>Update <code>just@latest</code> to 1.58.0.</p> </li> <li> <p>Update <code>jaq@latest</code> to 3.1.1.</p> </li> <li> <p>Update <code>cargo-nextest@latest</code> to 0.9.143.</p> </li> <li> <p>Update <code>cargo-crap@latest</code> to 0.4.2.</p> </li> <li> <p>Update <code>biome@latest</code> to 2.5.7.</p> </li> </ul> </blockquote> </details> <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md">taiki-e/install-action's changelog</a>.</em></p> <blockquote> <h2>[2.85.10] - 2026-08-07</h2> <ul> <li> <p>Update <code>uv@latest</code> to 0.12.2.</p> </li> <li> <p>Update <code>tombi@latest</code> to 1.2.7.</p> </li> <li> <p>Update <code>cosign@latest</code> to 3.1.3.</p> </li> <li> <p>Update <code>coreutils@latest</code> to 0.10.0.</p> </li> <li> <p>Update <code>cargo-rdme@latest</code> to 2.2.0.</p> </li> <li> <p>Update <code>cargo-crap@latest</code> to 0.4.3.</p> </li> </ul> <h2>[2.85.9] - 2026-08-06</h2> <ul> <li> <p>Update <code>zola@latest</code> to 0.23.1.</p> </li> <li> <p>Update <code>wild@latest</code> to 0.10.0.</p> </li> <li> <p>Update <code>mise@latest</code> to 2026.8.2.</p> </li> <li> <p>Update <code>just@latest</code> to 1.58.0.</p> </li> <li> <p>Update <code>jaq@latest</code> to 3.1.1.</p> </li> <li> <p>Update <code>cargo-nextest@latest</code> to 0.9.143.</p> </li> <li> <p>Update <code>cargo-crap@latest</code> to 0.4.2.</p> </li> <li> <p>Update <code>biome@latest</code> to 2.5.7.</p> </li> </ul> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/taiki-e/install-action/commit/6c6fd71fe4fb72c3697d269963d0e15df8adedad"><code>6c6fd71</code></a> Release 2.85.10</li> <li><a href="https://github.com/taiki-e/install-action/commit/37cec23487191ef9aef8b1315865bd6dc584bef4"><code>37cec23</code></a> Update zola manifest</li> <li><a href="https://github.com/taiki-e/install-action/commit/4914ea4fea5759852f8cda51753420130b883d65"><code>4914ea4</code></a> Update <code>uv@latest</code> to 0.12.2</li> <li><a href="https://github.com/taiki-e/install-action/commit/0ce64163d455e35fedc5f5925221d4a71f05c990"><code>0ce6416</code></a> Update <code>tombi@latest</code> to 1.2.7</li> <li><a href="https://github.com/taiki-e/install-action/commit/1f89e2fb52c482b53fcf083bf64b5abd5d6563ab"><code>1f89e2f</code></a> Update osv-scanner manifest</li> <li><a href="https://github.com/taiki-e/install-action/commit/65ef13f21e6dd200e402682be3b1bc896f6aa862"><code>65ef13f</code></a> Update kingfisher manifest</li> <li><a href="https://github.com/taiki-e/install-action/commit/9f6a5a8ec701c59d62ac1013b92714e7410b2cae"><code>9f6a5a8</code></a> Update <code>cosign@latest</code> to 3.1.3</li> <li><a href="https://github.com/taiki-e/install-action/commit/df08c38f9c0580a749efefc712ba563ff2107db5"><code>df08c38</code></a> Update <code>coreutils@latest</code> to 0.10.0</li> <li><a href="https://github.com/taiki-e/install-action/commit/dc7bb1f807a876bf372de55372b320860efe4019"><code>dc7bb1f</code></a> Update <code>cargo-rdme@latest</code> to 2.2.0</li> <li><a href="https://github.com/taiki-e/install-action/commit/9970698e35256036b84ecfb8a70b90fe068a934b"><code>9970698</code></a> Update <code>cargo-crap@latest</code> to 0.4.3</li> <li>Additional commits viewable in <a href="https://github.com/taiki-e/install-action/compare/v2.85.8...v2.85.10">compare view</a></li> </ul> </details> <br /> [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) </details> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
581e1129fa |
test(remove): pin the main-worktree exclusion, and say what decides it
`compute_worktree_path` returns the repo root for the default branch of a non-bare repo, so a detached main worktree passes every predicate in `detached_worktree_for` except `is_linked()` — which is the only thing stopping the removal from pointing a user at a directory `wt remove` refuses outright. Nothing pinned that: the existing default-branch test never reaches the annotation, because `check_not_default_branch` errors first and only fires when the removal isn't forced. `-D` skips that error and runs the whole way through, so that is where the test belongs. Verified by dropping the predicate, which makes the new snapshot name the repo root. The docstring said the exclusion exists because "the hint would name a command that can't run", which a locked worktree also satisfies while being named all the same. What actually decides it is whether the hint leads anywhere: a lock refusal carries the `git worktree unlock` that clears the way, and the main worktree's carries nothing. Co-Authored-By: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
acdbbb6824 |
fix(remove): name the detached worktree instead of refusing the removal
The guard this branch added was solving the wrong problem. Detaching a worktree's HEAD severs the only link git records between it and the branch, so the branch really has no worktree and deleting the ref alone is the correct operation — and it never lost anything: `SafeDelete` retains an unintegrated branch, so the only refs it deleted were ones worktrunk's own integration test calls lossless, and even a forced `-D` leaves the commits reachable through the detached worktree's HEAD, which is a GC root. What #3769 actually reported was silence. `○ No worktree found for branch <name>` is true and still reads as "nothing is there" while a directory sits at exactly that path, so the removal now names it and the `wt remove <path>` that clears it, on stderr and in `--format=json`'s new `detached_worktree`. Refusing instead cost a legitimate branch cleanup and let a branch address a detached worktree — the one thing the worktree model says a branch cannot name. `detached_worktree_for` survives to find the path; it no longer decides whether the command runs, which is also why the guard-scoping it needed (a branch-existence check, a `deletion_mode` gate) goes with it: an extra line of output is harmless where a refusal had to be narrowed case by case. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
8bf8ae44a5 |
docs(release): scope the changelog length rule to bullets, on a three-point scale (#3798)
Two corrections to the changelog length rule [#3774](https://github.com/max-sixty/worktrunk/pull/3774) added, both surfaced by writing 0.72.0's section to it in [#3778](https://github.com/max-sixty/worktrunk/pull/3778). **The whole-release ~1,200-word ceiling is gone.** It gated a number dominated by things that aren't prose: of that section's 1,314 measured words, 320 are the bold entry titles and 76 are trailing PR links and `thanks @…` credits, leaving 918 words of description. A release-level total also scales with entry count, so a release with more changes reads as more verbose without any single bullet being longer. The per-bullet numbers are the rule; the release total was a second, worse proxy for the same thing. **One number became a scale: 35 typical, 60 for a dense entry, 80 for the two or three headline entries.** A flat 40 put more than half of a carefully-trimmed section in violation, which makes the rule noise rather than a signal. 60 is roughly 43 words of description once the ~17 words of title, links, and credit are subtracted — two full sentences, which is what an entry combining several PRs actually needs. The measuring command reported the release total. It now reports the average and the count over 60: ``` 30 entries, 43 avg, 3 over 60 ``` Worth flagging against this change: on 0.72.0's section those 3 *are* the designated headline entries, so the outer bound flags nothing there. The live signal is the average — 43 against a 35 target. The verification gate flags entries over 60 to match. > _This was written by Claude Code on behalf of max-sixty_ Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
dee7d77ae7 |
docs(remove): scope the detached-worktree note to removals that delete a ref
The paragraph was written when the guard was unconditional, so it promised a refusal that `--no-delete-branch` and `[remove] delete-branch = false` turn off — and those are the users most likely to go looking, since what they actually see is the inaccurate `○ No worktree found for branch …` that #3769 opens with. Co-Authored-By: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
56ada0bf23 |
fix(remove): scope the detached-worktree guard to removals that delete a ref
The guard fired ahead of `prepare_worktree_removal`'s branch-existence check, so a name that is not a local branch but whose templated path holds a detached worktree reported `Branch <name> has no worktree` instead of `No branch named <name>`. Check `exists_locally` first. It also fired under `--no-delete-branch` / `[remove] delete-branch = false`, where the branch-only arm deletes nothing — nothing was going to strand the worktree, so the guard turned a no-op exit 0 into a hard failure. Gate on `deletion_mode.should_keep()`. An unintegrated branch under `SafeDelete` retains its ref too, but only the deletion attempt downstream knows that, so those still refuse rather than exiting 0. That is the conservative direction — the worktree the refusal names is on disk — and a test pins it as a decision rather than an accident. Co-Authored-By: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
897f7d7193 |
fix(tests): extend the spawn pin to benches; correct the pin's cost note (#3792)
Follow-up to #3784, prompted by a history audit of the spawn-flake family. Benches still spawned `env!("CARGO_BIN_EXE_wt")` — the uplifted path the suite stopped spawning — so a concurrent build could fail a bench run's spawns; all 11 sites now route through `wt_bin()` and `test_wt_spawns_are_pinned` scans `benches/` too, making the rule exceptionless. The pin's docstring also claimed the hardlink shares the `deps/` artifact's inode: true where cargo uplifts by hardlink (Linux), but macOS uplifts by copy-on-write clone — the pin keeps the clone, whose blocks stay shared with `deps/` (measured: cloning the 70 MB binary consumes 8 KB), so the no-cost conclusion stands with the mechanism now stated per platform, plus why nothing sweeps the directory. > _This was written by Claude Code on behalf of max-sixty_ --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
27a0e21542 |
fix(errors): stop double-escaping the path in worktree-removal hints
`format_path_for_display` already returns a shell-ready token, so routing
its result through `suggest_command` escapes it a second time. A worktree
under $HOME rendered as `wt remove '~/repo.feature'`, where the quoting
suppresses the tilde expansion the unquoted form in the line above it
relies on; a path that genuinely needs escaping came out carrying literal
quote characters and resolved to nothing.
The three hints that composed the two now interpolate the formatted path
directly, as the neighbouring `rm -rf {path}` and `git worktree unlock
{path}` hints already do. `format_path_for_display` documents the trap so
the next caller doesn't repeat it — `pr_mr_switch_hint` had recorded it
only in its own docstring.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
cc4aacf99f |
fix(remove): refuse to strand a detached worktree when removing by branch
Detaching a worktree's HEAD severs the only link git records between it and the branch, so `wt remove <branch>` dropped out of the branch-first lookup, degraded to a branch-only deletion, and exited 0 — deleting the ref and leaving the worktree registered. The failing half was silent. The `worktree-path` template is what still connects the two, so the removal matches against it and refuses, naming the path that reaches the worktree. Three states are deliberately not matched: a worktree on some other branch (that branch names it), the main worktree (whose path-based removal refuses too, so the hint would be a dead end), and a prunable entry (stale metadata, not a worktree on disk). The guard sits in `wt remove` rather than `prepare_worktree_removal`, which every producer of a branch-only target shares: `wt step prune` plans its whole sweep from one worktree-list snapshot, so a detached worktree it is about to remove as its own candidate is still registered when the branch's plan is built — refusing there would leave prune unable to clean up either half. `live_sibling_checkout` keeps `exists()` rather than the union predicate, and now says why: the two only disagree on a directory that is present but no longer holds its worktree, where calling it dead deletes a branch a checkout still resolves. Fixes #3769 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
f13ef96637 |
docs(worktree): correct three specs the selector refactor left stale (#3786)
Three specs in #3785's selector refactor describe mechanisms that change removed or altered. Comment-only; no behavior change. Each is wrong in a way a reader would act on rather than merely notice: **`worktree_is_unusable`** closed with "`false` for a path git has no registration for". The `!path.exists()` early return makes that untrue as a claim about the return value — such a path answers `true` when it is simply gone. The sentence was only ever about the `prunable` lookup, so it now says so, and records that no caller reaches the combination (all three take their path out of the listing). **`normalize_selector`** named three call sites, two of which no longer call it, and spent a paragraph explaining the string-comparison design the refactor deleted — documenting a removed mechanism as current, which is the worst of the three. It now names its one home and says what made a single home possible. **`ResolvedTarget::selector`** credited only a rewrite with taking the path arm off, missing `--create`, which takes it off without rewriting anything. The first was raised on #3785 after I had worked its other threads, so it never got a reply there. The other two came from re-reading the neighbouring specs while fixing it. <details> <summary>Why these were worth a change rather than a note</summary> A spec that describes a deleted mechanism is worse than no spec: `normalize_selector`'s explained why each assembly of the resolution ladder had to normalize for itself, which was true before `Selector` carried "may this token name a path?" as a fact rather than a string comparison. A reader adding a fourth entry point would have followed it and re-introduced the per-site normalization the refactor removed. </details> > _This was written by Claude Code on behalf of max-sixty_ |
||
|
|
715af4cd72 |
fix(tests): pin the spawned wt binary against concurrent cargo uplifts (#3784)
Two test-suite flakes fixed at the root, both dependencies on machine
load and concurrent builds.
**Concurrent-cargo spawn `NotFound`.** Cargo uplifts `target/debug/wt`
by removing the path and recreating it, so a second `cargo` against the
same target directory leaves the binary every test spawns absent for a
fraction of a millisecond per rebuild — the one-off `NotFound` spawn
failures that pass on re-run. `wt_bin()` now returns a hardlink pinned
under `target/debug/wt-test-bin/<mtime>-<len>/`: the uplift unlinks only
the uplifted name, so the pin keeps serving the observed binary through
any number of concurrent rebuilds, at no disk cost beyond the `deps/`
artifact whose inode it shares. `test_wt_spawns_are_pinned` keeps every
spawn routed through it. Reproduced by re-creating the uplift every 200
ms alongside a full `cargo nextest run`: 302 of 4583 tests failed before
this change (147 as direct `NotFound` spawn panics, five of them
byte-identical to the original shell-wrapper report), 4585 of 4585 after
— with an unrelated external cargo also rebuilding `wt` mid-validation,
absorbed the same way.
**`--reap` probe races.** `test_remove_reap_kills_process` predicted the
reap guard's verdict with its own `lsof`/`ps` snapshot, and under load
either probe's spawn can stall past the 5 s bound, whose fail-safe empty
result flips the outcome — a prediction `wt` then contradicts, or `wt`
reporting "No processes to reap" for a live child. The prediction now
reads the session's controlling terminal directly (`/dev/tty` opens iff
the session has one — the property the child inherits at spawn), the
probe timeout is env-pinnable (`WORKTRUNK_TEST_PROBE_TIMEOUT_MS`, set to
60 s in the static test baseline; production keeps its 5 s bound), and
the discovery poll uses the suite's 60 s presence-poll convention.
Looped 15/15 green at load average ~60, where the previous shape failed
2/10.
Not covered here, noted as follow-ups: benches still spawn
`env!("CARGO_BIN_EXE_wt")` directly (same hazard, separate runner,
outside the guard's scan), and the reap "spared" branch has no
deterministic end-to-end test (needs a PTY-held child).
> _This was written by Claude Code on behalf of max-sixty_
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
cf822f75cf |
fix(worktree): guard a registered path that no longer holds its worktree (#3785)
A registered worktree's path was trusted to still hold that worktree.
Three defects followed, one of them destroying data, and the check that
would have caught them — where it existed at all — was `Path::exists()`.
## `wt remove --force` deleted an unrelated repository
A clone that came to sit at a stale registration's path was removed
whole, including uncommitted work and, for a repo never pushed, the only
copy of its objects. wt's own hint routed the user there: the dirty gate
reads `git status` in that directory, reports the occupant's changes as
this worktree's, and offers `--force` as the cure.
```console
$ wt remove feature
✗ Cannot remove worktree: feature has uncommitted changes
?? precious.txt # ← the other repo's file
↳ ... to lose uncommitted changes, run wt remove --force feature
```
git refuses that same removal, `--force` included (`validation failed …
is not a .git file`). Worktrunk's fast path renames the directory into
trash rather than asking git to, so git's validation never ran.
`ensure_belongs_to_repo` makes it, comparing the directory's git dir
against this repository's: a linked worktree's sits under
`<common>/worktrees/`, the main worktree's *is* the common dir, anything
else answers to someone else. One comparison covers both worktree kinds
and also rejects a `.git` file pointing at another repo, which git's
shape test accepts.
It runs at planning, ahead of the dirty gate, and again at the rename
for callers that stage without planning. Now:
```console
$ wt remove --force feature
✗ Directory @ ../repo.feature is not this repository's worktree
↳ Removing it could destroy unrelated data; move the directory aside, then run git worktree prune
```
## A recreated worktree directory leaked git's exit 128
`wt switch`, `wt merge`, and `wt step push` walked into `git rev-parse
--git-dir failed (exit 128)`. Two of them probed `Path::exists()` first,
which a deleted-and-recreated directory passes; the third asked nothing.
`worktree_is_unusable` is the union of both tests, because neither
implies the other. `exists()` catches the absent directory; git's
`prunable` catches the recreated one. `prunable` alone is *not* the
wider test it looks like — git withholds the attribute from a **locked**
worktree even when its directory is gone, since prunability is its
pruning policy and a lock means "don't prune this":
```console
$ git worktree list --porcelain # wt2 locked, all three directories removed
worktree /tmp/ptest/wt1
prunable gitdir file points to non-existent location
worktree /tmp/ptest/wt2
locked removable media # ← no prunable line
worktree /tmp/ptest/wt3
prunable gitdir file points to non-existent location
```
A locked worktree on an unmounted volume is exactly what
`prepare_worktree_removal`'s lock guard exists for, so a `prunable`-only
test would read it as healthy. All three commands now give the message
the merely-deleted case already gave.
`wt remove` keeps `exists()`, deliberately: there it is the precondition
for the cleanup path rather than a health test, since
`prune_worktree_entry` unregisters via `git worktree remove`, which
skips validation only while the directory is absent. No scoped git
command clears the recreated case, so it reports and names the repo-wide
`git worktree prune` that does.
## `wt switch docs/` missed a branch sitting right there
Git's ref format forbids a trailing `/`, so the branch lookup never had
a candidate — and shell completion produces exactly that spelling
whenever a `docs` directory sits beside the branch. Selectors are
normalized before resolution.
## Resolving selectors through one ladder
The three fixes landed in three of the four places that assemble "expand
shortcuts, try the branch, try the path, classify the failure" by hand.
Each gated its path attempt on "did something rewrite this token?",
answered by comparing an expansion's output against its input:
| where | the comparison |
|---|---|
| `resolve_worktree` | `branch == name` |
| `plan_switch` | `target.branch == branch` |
| `target_worktree_at_path` | `target.filter(\|t\| *t == resolved)` |
| `resolve_base_ref` | `resolved == base` |
That is a fact the rewriting step knows, re-derived downstream from its
output, and it is wrong in both directions. A shortcut can expand to the
token it was given — `-` pointing at the branch you are already on — and
string equality reads that as a literal, turning the path arm back on
for a token nobody typed. Normalization breaks it the other way, which
is why the trailing-separator fix needed threading through three call
sites.
`Selector` carries the fact instead: `expand_shortcut` reports whether
it fired, `wt switch` reports its `pr:`/`mr:` dispatch and remote-prefix
strip, and `names_a_path()` replaces all four comparisons.
`resolve_selector` is the ladder, and `plan_switch` expands into it
rather than re-implementing its phases.
`names_a_path()` gates both path steps together — the worktree-by-path
lookup and the directory verdict — which is what `wt switch --create`
needs: the argument names a branch to create, so `branch_only()` takes
the arm off at the producer rather than each consumer re-testing
`create`.
It also reaches the directory verdict, so `ResolvedWorktree` gains
`NoWorktreeAtPath` and the four sites that called `path_selector_error`
themselves stop re-deriving it. The docstring defending that laziness
didn't survive checking — the function returns on `is_valid_branch_name`
before touching the filesystem, so every ordinary branch name already
short-circuited.
| | before | after |
|---|---|---|
| `normalize_selector` call sites | 3 | 1 |
| `path_selector_*` call sites | 4 | 2 |
| "was it rewritten?" comparisons | 4 | 0 |
## Navigating the diff
- `src/git/repository/mod.rs` — `Selector`, `normalize_selector`, the
new `ResolvedWorktree` variant.
- `src/git/repository/worktrees.rs` — `expand_shortcut`,
`expand_selector`, `resolve_selector`, `usable_worktree_for_branch`.
- `src/git/repository/working_tree.rs` — `ensure_belongs_to_repo`, the
ownership check.
- `src/git/remove.rs`, `src/commands/repository_ext.rs` — where it gates
removal, and why before the dirty gate.
- Call sites: `commands/worktree/switch.rs`,
`commands/worktree/push.rs`, `commands/merge.rs`, `commands/remove.rs`,
`git/repository/config.rs`.
## Size
Comments and docstrings are the largest share: the ownership check and
the four conditions behind the directory verdict all look like things to
simplify away, so the reason each exists is recorded where it's
enforced.
| | + | − |
|---|---:|---:|
| Production code | 277 | 142 |
| Comments & docstrings | 320 | 75 |
| Tests | 325 | 8 |
| Snapshots | 186 | 0 |
| Docs | 6 | 0 |
| **Total** | **1114** | **225** |
## Testing
Seven new tests. The data-safety one drives the real binary and asserts
the filesystem afterwards, not just the exit code — removal stages by
rename and deletes in a detached process, so a passing exit would not
have caught a staged-then-deleted tree. The others cover the recreated
directory (switch and remove), the trailing separator, `--create`
against a worktree registered at that path, and, at the unit boundary,
the four states of `worktree_is_unusable` — healthy, absent,
locked-and-absent, recreated — and the selector's path-ness, including
the degenerate case string equality got wrong. Each new test was
confirmed to fail with its fix reverted.
One more covers an omitted merge target in a repo whose default branch
can't be determined. `^` had a test for that error; the omitted-target
route to the same message had none. The gap predates this branch — the
closure is byte-identical to the one it replaces and codecov records
those lines as missed at the base commit too — but relocating them into
`resolve_target_selector` re-counted them as patch lines, which is what
surfaced it.
Local gate green: 4593 tests, lints, doctests, rustdoc under
`-Dwarnings`.
<details>
<summary>Behavioral matrix, verified against a build</summary>
```console
docs/ (trailing sep) ▲ Worktree for docs @ ../repo.docs
detached by path ▲ Worktree for detached worktree @ ../repo.det
leftover dir ✗ No worktree @ ../repo.leftover
recreated dir ✗ Worktree directory missing for rec
shortcut ^ ▲ Worktree for main @ ../repo
remove leftover ✗ No worktree @ ../repo.leftover
--base docs/ ✓ Created branch nf from docs
foreign-repo remove ✗ Directory @ ../repo.frn is not this repository's worktree
precious.txt survives
```
</details>
<details>
<summary>Also swept, and one thing left alone</summary>
Three more instances of the same shape, fixed here:
- `resolve_base_ref` was the fourth copy of the comparison, so `--base
docs/` now resolves too.
- `hint_for_repo` suggested `wt switch ^` after an existence probe a
recreated directory passes, pointing at a worktree the switch then
refuses.
- The pre-switch hook's `target` var used the bare shortcut expander, so
a hook saw `docs/` where the switch resolved `docs`.
The identical unborn/stale default-branch block in
`require_target_branch` and `require_target_ref` is extracted. The rest
of that pair differs in its existence predicate, extra arms, and final
error; sharing it would cost more in parameters than the duplication
does.
Left alone: `live_sibling_checkout` decides whether another worktree
still holds a branch during removal, and also uses `exists()`. Switching
it to `prunable` would make branch deletion *more* likely in a corner
case where the detached path already answers the other way. That is a
data-safety surface and a separate decision.
</details>
> _This was written by Claude Code on behalf of max-sixty_
|
||
|
|
a41cdb826c |
docs(config): name every template variable format_path builds (#3782)
Found by the nightly survey while reviewing
`src/config/user/accessors.rs`.
`UserConfig::format_path`'s `# Arguments` block doesn't match what the
function builds. It documents `{{ main_worktree }}`, `{{ branch }}`, and
`{{ owner }}`, and describes the `repo` parameter as being for "template
function access" — but the function also inserts `{{ repo }}` (from
`main_worktree`, not from `repo`) and `{{ repo_path }}` (read off
`repo`). Both omissions are in `default_worktree_path()` in the same
file — `"{{ repo_path }}/../{{ repo }}.{{ branch | sanitize }}"` — so
the docstring omits two of the three variables the default template
uses.
The `{{ owner }}` line was also a bare bullet in the middle of the
parameter list, describing a template variable rather than an argument,
and it didn't note that `owner` is only inserted when
`primary_remote_parsed_url()` returns a URL it can parse.
This corrects the parameter descriptions, moves the template-variable
note out of the argument list, and links the user-facing variable list
at
[worktrunk.dev/config](https://worktrunk.dev/config/#worktree-path-template)
rather than duplicating it here (verified: the page returns 200 and
carries that anchor).
No test: the change is a doc comment only, with no behavior to exercise.
`cargo fmt --check` and `cargo doc --no-deps -p worktrunk` are clean.
Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
|
||
|
|
ec0f3a344f |
fix(output): strip stderr color on a pipe where std's eprint! kept it (#3771)
## Problem
`worktrunk::styling`'s `eprint!` is anstream's and strips ANSI when
stderr isn't a terminal; std's prelude macro of the same name keeps it.
A file that imports one but not the other — or neither — gets a mix, and
adjacent lines of the same message block disagree about whether a
redirected stderr carries escapes.
`wt list 2>&1 >/dev/null | cat -v` in a repo with a deprecated `[ci]`
block, on `main`:
```
^[[33mM-bM-^VM-2^[[39m ^[[33mProject config: ^[[1m[ci]^[[22m is deprecated in favor of ^[[1m[forge]^[[22m^[[39m
M-bM-^FM-3 To see details, run wt config show; to apply updates, run wt config update
```
The warning is `eprint!("{warnings}")` at `src/config/deprecation.rs`,
which resolved to std's macro; the hint directly beneath it is the
`eprintln!` imported from `styling` four lines later. Anyone redirecting
`wt` narration to a file gets escapes on one line and not the next.
This is the stderr counterpart of what #3746 and #3766 fixed on stdout,
and it went unnoticed for the reason named in `verbatim.rs`'s own
docstring: the suite sets `CLICOLOR_FORCE=1`, which forces color on
*both* printers, so no snapshot could disagree no matter which macro was
in scope. `output_system_guard` doesn't cover it either — it scans for
`print!`/`println!` tokens under `src/commands/`, not for which
`eprint!` a file imported.
## Solution
The rule is now structural rather than per-site.
`check_stderr_macros_come_from_styling` in `output_system_guard.rs`
walks every `.rs` file under `src/` and flags a bare
`eprint!`/`eprintln!` whose file lacks the matching `worktrunk::styling`
import. A call satisfies it either way — importing the macro, or
qualifying the call as `styling::eprintln!(…)`, which several files
(`git/repository/mod.rs`, `config/user/mod.rs`,
`commands/config/alias.rs`) already do. Two files are allowlisted with a
reason: `testing/mock_stub.rs` relays a stub's captured stderr verbatim,
so its bytes are fixture data; `remove_dir.rs`'s one call is a
`#[cfg(test)]` skip diagnostic, not narration a user redirects.
Reverting the source fixes below makes it name exactly those five lines
and nothing else.
The sites it fixes:
- `src/config/deprecation.rs` — the deprecation warning block above.
- `src/commands/config/update.rs` — the `format_update_preview` block
shown before `wt config update`'s prompt, reachable with a tty stdin and
a redirected stderr.
- `src/output/prompt.rs` — the `[y/N/?]` prompt; its blank-line
`eprintln!` was already explicitly qualified as
`worktrunk::styling::eprintln!`, so the two disagreed within four lines.
Both now come from one import.
- `src/output/global.rs` — the file the first scan couldn't see, because
that scan looked for "imported `eprintln` but not `eprint`" and this
file imports neither. Its four styled `eprintln!` calls
(`print_outdated_shell_wrapper_hint_once`, `warn_retired_exec_once`,
`warn_exec_scrubbed_once`) all resolve to std's, so a user mid-upgrade
running `wt … 2>log` gets `ESC[…m` around the shell-wrapper repair hint.
The module's own docstring already claimed the contract the code didn't
have — *"Regular output still uses `eprintln!`/`println!` directly (from
`worktrunk::styling` for color support)"*. One added import makes it
true; under the suite's `CLICOLOR_FORCE=1` no snapshot moves.
- `src/commands/for_each.rs` — the pre-spawn ANSI reset.
`output/handlers.rs` runs the identical three lines
(`stderr().flush()?`, `eprint!("{}", anstyle::Reset)`,
`stderr().flush().ok()`) immediately before building its `Cmd`, but
through anstream's `eprint` *and* anstream's `stderr`; `for_each` used
std's for both, so the same operation wrote a literal `ESC[0m` into a
redirected stderr where `handlers` dropped it. Both halves move together
— the flushes have to name the stream the reset was written to, so
switching `eprint!` alone would flush std's handle while anstream's
buffer held the write. The `std::io::stderr()` handed to `Stdio::from`
four lines down is a different thing and stays.
## Tests
`test_stderr_narration_strips_ansi_when_piped` in
`output_system_guard.rs`, alongside the closed-consumer test #3766
added. It clears `CLICOLOR_FORCE` and sets `NO_COLOR` (which only
anstream honors), triggers the `[ci]` deprecation, and asserts stderr
carries the warning and no `\x1b`. Confirmed to fail on the pre-fix
source with exactly the escapes quoted above, and to pass with it.
That test proves what the property buys at one site;
`check_stderr_macros_come_from_styling` is what holds it at all of them.
No runtime test can: the suite's `CLICOLOR_FORCE=1` forces color on both
printers, so a snapshot agrees whichever macro is in scope, and the
property is about every stderr write in the binary rather than any one
path.
The module docstring is updated for both — the `Allowed:` list no longer
reads flatly as "`eprintln!` / `eprint!` (stderr is safe)", which was
the sentence someone skims before making this exact mistake.
## Verification
`cargo clippy --all-targets`, `cargo fmt --check`, `cargo test --lib
--bins` (2,454 passed), and the integration suite (1,961 passed) all run
locally. One integration test fails in this sandbox and is unrelated:
`test_copy_ignored_preserves_file_executable_permissions` expects `0644`
and sees `0664`, because the sandbox's umask is `002` rather than the
runner's `022` (confirmed by `umask` → `0002`). It touches none of these
files; CI will confirm.
---------
Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
|
||
|
|
ec574b6c35 |
ci: bump pinned cargo-nextest 0.9.143 and worktrunk 0.72.0 (#3783)
## Summary Weekly CI pin check found the following drift (these inline `version:` strings are invisible to Dependabot — it follows `Cargo.toml` deps and `uses: foo@vN` refs, not pinned versions inside `with:` blocks): - `cargo-nextest`: 0.9.140 → 0.9.143 (MSRV 1.91, compatible with our 1.96) — pinned in `coverage.yaml`, `actions/test-setup`, and `actions/claude-setup`; all three moved together. - `worktrunk`: 0.71.0 → 0.72.0 (MSRV 1.96, compatible with our 1.96) — the CI-installed `wt` that runs `wt hook pre-merge`, bumped to the current release; pinned in `ci.yaml` (×2) and `nightly.yaml`. ## Already up to date - `cargo-affected`: 0.4.0, `cargo-insta`: 1.48.0, `cargo-llvm-cov`: 0.8.7, `cargo-msrv`: 0.19.3, `cargo-udeps`: 0.1.61, `lychee`: 0.24.2 - `hustcer/setup-nu` (nushell): 0.114.1 — matches the current nushell release across all four call sites - Runner images: ubuntu-24.04, windows-2022 ## Notes - windows-2022 stays pinned ([actions/runner-images#12677](https://github.com/actions/runner-images/issues/12677) — windows-2025 lacks the D: drive). - **cargo-nextest 0.9.143 has nothing config-facing to adjust.** The 0.9.140 → 0.9.143 range is dynamic-library-search-path fixes (build-dir layout v2, `build.build-dir`, `[[example]]` targets), archive filterset fixes, an opt-in `junit.report-skipped` setting we don't set, and a listing progress bar. The one behavior change — ordering the Cargo artifact directory ahead of `deps` on the dylib search path, matching Cargo since 1.93 — doesn't affect this repo, which links no `dylib` dependency. - **worktrunk 0.72.0 is only exercised through `wt hook pre-merge`** in these three jobs, so the release's `wt merge` / `wt step push` two-tree changes and the `branch_outcome` JSON rename don't reach CI. The relevant one is the opposite direction: 0.72.0 fixes `wt` writing ANSI to a pipe and exiting 101 on `Broken pipe`, which is exactly the non-tty shape these jobs run in. - **`zola` is deliberately left at 0.22.1** — see below. ## Deferred: zola 0.22.1 → 0.23.2 `taiki-e/install-action`'s `tool: zola@0.22.1` in `check-docs` (and the matching pin in `publish-docs.yaml`) is behind, but 0.23.0 is not a routine bump. Upstream calls it "probably the most breaking version of Zola that will happen" ([CHANGELOG](https://github.com/getzola/zola/blob/master/CHANGELOG.md)): **shortcodes are removed entirely** and Tera is updated to v2 with its own [migration guide](https://github.com/Keats/tera/blob/master/MIGRATION.md). `docs/templates/shortcodes/` and `docs/templates/macros.html` both exist, so this needs a real docs-site migration rather than a version-string change, and it would land in the same PR as the live-site publish pin. Left for a separate change; flagging it here so it isn't silently skipped each week. Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> |
||
|
|
f0d9c725a4 |
docs(tend): bound the data-loss hold to what the deletion can reach (#3779)
The data-loss hold in the tend review reference was purely lexical: any deletion token appearing in a diff matched, with no bound on what the deletion could reach. On #3776 that meant holding on `rm -rf "$TEMP"` against a `mktemp -d` the same script created three lines earlier, and on `rm -f` against the container's apt lists inside `task setup-web`. Neither can touch a worktree. The trigger list is unchanged. Two things now bound it: - The first clause fires on adding a deletion or on widening what an existing one can delete, so restructuring one that keeps firing on the same or fewer paths is no longer a match. - A new paragraph names the surface the hold protects: a worktree, a repository, a branch, uncommitted work, or a file worktrunk writes on the user's behalf — the inventory in CLAUDE.md § Data Safety. A deletion reaches none of it when everything under its target can be regenerated, or when it is confined to a throwaway CI or development environment. The carve-out turns on contents, not on how recently the directory was created. `wt step promote` is the in-repo counterexample to the naive version: `stage_ignored` moves both worktrees' ignored files into `.git/wt/staging/promote` and `distribute_staged` removes the directory at the end of the same operation, so a directory the code just created holds the user's only copy in between — which is why `check_leftover_staging` refuses to proceed when it survives. Checked against cases that must still hold: `remove_dir_all` on a worktree path, `git clean -fdx` in a shipped hook, and a Taskfile step deleting `.git/wt/` state all match, since none of those targets is regenerable or confined to a throwaway environment. This file lists `rm -rf` and its neighbours as review triggers, so the diff edits a file that contains one. It executes nothing. `cargo run -- hook pre-merge --yes` passes: 4583 tests, 1 skipped. > _This was written by Claude Code on behalf of max-sixty_ --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
4d7f8c944e |
fix(env): drop Intel macOS from the flake, and install pwsh and jq for web setup (#3776)
Three follow-ups from #3768, plus a bug the verification turned up. **The flake stops declaring outputs for `x86_64-darwin`.** nixpkgs drops Intel macOS in 26.11: evaluating anything for that system against `nixos-unstable` (`26.11pre-git`) throws. The rev `flake.lock` pins is 26.05-era, so it still evaluates today, carrying nixpkgs' own warning that "26.05 will be the last release to support x86_64-darwin". Naming three systems rather than `eachDefaultSystem` drops Nix support for Intel Macs now, ahead of that bump. Release binaries are untouched: `dist-workspace.toml` still ships `x86_64-apple-darwin` and nightly still tests it on `macos-15-intel`. **`git` leaves the devShell's `packages`.** It arrives with the `checks`, which crane folds in via `inputsFrom`, the same mechanism that already supplied `python3`, `procps` and `lsof`. **`task setup-web` installs `pwsh` and `jq`.** Without them Claude Code web can't run `--features shell-integration-tests`, which is what the pre-merge gate runs. PowerShell comes from the release `.deb` rather than the tarball, because pwsh aborts at startup without libicu and only the `.deb` declares that dependency for apt to resolve. The verification loop runs each tool instead of looking for it on PATH, since the tarball install left a `pwsh` that was on PATH and still aborted. **A `set -e` abort found while testing that.** The `sources.list.d` cleanup was an `&&` chain, and under `set -e` a chain ending false takes the whole task down. This one ends false on an unmatched glob and on a `.list` file with no `[` line, so setup was dying before it installed anything on a stock Debian box as well as an empty one. It's an `if` now. ## Verification No `nix` on the machine this was written on, so the flake was checked in a `nixos/nix` container and the Taskfile block in an amd64 Debian one. <details> <summary>flake: three systems evaluate, x86_64-darwin is gone, git survives its deletion</summary> ``` == devShell evaluates per system == x86_64-linux OK g172vwl0g339zsxx9l6mz5pca6w9jbcx-nix-shell.drv aarch64-linux OK pgvq71zs48bx3naddncms954jyqpl0bl-nix-shell.drv aarch64-darwin OK d17q1772si0x0hj1lgpnin8wiq4zlpr2-nix-shell.drv x86_64-darwin FAIL: flake does not provide attribute 'devShells.x86_64-darwin.default' == systems the flake declares == ["aarch64-darwin","aarch64-linux","x86_64-linux"] == tools in the x86_64-linux devShell == git: present jq: present nushell: present powershell: present python3: present procps: present lsof: present fish: present zsh: present bash: present gh: present pre-commit: present == nixfmt --check flake.nix == clean (exit 0) ``` The x86_64-darwin claim, checked against nixpkgs directly rather than inferred: ``` == nixos-unstable lib.version == "26.11pre-git" == x86_64-darwin eval on nixos-unstable == error, pointing at release-notes#x86_64-darwin-26.11 == x86_64-darwin eval on the pinned rev (flake.lock) == evaluation warning: Nixpkgs 26.05 will be the last release to support x86_64-darwin "hello-2.12.3" ``` Not verified: nothing was built, only evaluated. The nightly `nix-flake` job runs `nix flake check` on PRs touching `flake.nix`, which covers that on x86_64-linux. </details> <details> <summary>setup-web: the block run under Task's own interpreter, in an amd64 Debian container</summary> The edited block was extracted into a minimal Taskfile and run by `task` itself, so mvdan/sh parses it rather than bash. `curl` and nushell are container prereqs, not part of what's under test. ``` === running the extracted block under Task === Installing shell-integration test dependencies... pwsh installed bash available zsh available fish available nu available pwsh available jq available task exit: 0 === does the installed pwsh actually run? === 7.6.4 jq-1.6 /usr/bin/pwsh === rerun is idempotent === Installing shell-integration test dependencies... bash available zsh available fish available nu available pwsh available jq available ``` Two earlier runs are why the shape changed. The first died at the `sources.list.d` glob. The second installed PowerShell from the release tarball: every tool reported "available" and `pwsh` then aborted with `Couldn't find a valid ICU package installed on the system`, which is what moved the install to the `.deb` and the check from `command -v` to `--version`. </details> `cargo run -- hook pre-merge --yes` passes: 4574 tests, 1 skipped. ## Notes `task setup-web` still requires nushell to be present rather than installing it, unchanged here. > _This was written by Claude Code on behalf of max-sixty_ --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
d7fc65ba80 |
docs(changelog): rewrite 0.72.0's notes to the length and ordering standard (#3778)
0.72.0's notes ran 32 entries at 101 words each. [#3774](https://github.com/max-sixty/worktrunk/pull/3774) replaced "1–3 sentences" with a word ceiling and made reader interest the second ordering dimension inside each fixed section; this applies both to the release that motivated them, since the next release calibrates against this section. Every user-facing claim, PR link, and contributor credit carries over — the only dropped `@` is `@attacker.example` inside an example that went with its paragraph. Two pairs of related bullets are combined: the piped-stdout panic with the piped-color fix, and the three shell-wrapper cleanup PRs. Reordering, by section: - **Improved** now leads with the forge-host classification restored to self-hosters, then the `[projects]` glob entry, then the `wt merge` target-sync change that used to lead. The old first entry was the release's longest at 279 words and opened on its own mechanism. - **Fixed** leads with the piped-output panic and the CI status that read a running check as passed — the two most readers hit — and ends with the display and tally fixes. - **Internal** entries are one sentence each. ### Where it lands against the three ceilings The skill sets three numbers. Measured with its own `awk` command: **30 entries, 1,314 words, 43 average.** | | Ceiling | Here | | --- | --- | --- | | Per bullet | 40 | 17 entries above it, 13 at or under | | Headline band | 2–3 entries up to 80 | 3 (80, 78, 70) | | Whole release | ~1,200 | 1,314 | The whole-release number is 9% over and the per-bullet count misses on more than half the entries, so the standard is partly met, not met. What the measure counts is worth knowing before reading those two as prose bloat: of the 1,314 words, 320 are the bold entry titles and 76 are trailing PR links and `thanks @…` credits, leaving 918 words of actual description — **31 per entry**. Hitting 1,200 with 30 entries and three headliners means a typical bullet of ~34 measured words, or roughly 22 words of prose after a 10-word title. That is tighter than the 40 reads, and it may be the ceiling rather than this section that wants adjusting; I've left the skill alone here rather than widen this PR. The published [v0.72.0 release body](https://github.com/max-sixty/worktrunk/releases/tag/v0.72.0) is updated to match; its 17 assets and install sections are untouched. The CHANGELOG at the `v0.72.0` tag keeps the original text, so this is a `main`-only edit. > _This was written by Claude Code on behalf of max-sixty_ --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
c37d07d87a |
refactor(output): resolve color in one place via anstream's global ColorChoice (#3777)
`wt` had five independent mechanisms deciding whether escape sequences reach the consumer: the anstream macros, a `println_verbatim!` macro that bypassed them, `ProgressiveTable`'s raw stdout writes, clap's per-command `ColorChoice`, and a raw `anstyle::Reset` in `terminate_output`. This collapses them onto the ecosystem primitive: `anstream::ColorChoice::write_global()`, which `AutoStream::choice` consults before tty detection — the mechanism behind every `--color=always` flag. Color now resolves in exactly one place, and code emits styles freely while the stream decides what survives. For navigating the diff: - **Payload surfaces declare their choice once.** The statusline (`src/commands/statusline.rs`) declares `Always` — a shell prompt or Claude Code captures and re-renders its line, so the escapes are the answer, not presentation; `Always` outranks `NO_COLOR` per the no-color.org convention, preserving shipped behavior byte-for-byte. `--help-page` declares `Always`/`Never` per mode and `--help-md` declares `Never` (`src/help.rs`). `println_verbatim!` is deleted; these surfaces print through the ordinary macros. - **`help.rs` keeps one color mapping.** The five per-command `cmd.color(...)` calls were inert — clap consults `color_when` only in `err.print()`, and every path here uses `err.render()` — so the embedded `--help` reference block always renders `.ansi()` and the stream strips or passes it. All 16 generated doc outputs (`--help-page`, `--plain`, `--help-md` across commands) are byte-identical before/after. - **`ProgressiveTable` consults the same choice rather than writing through a stream** (`src/commands/list/progressive_table.rs`) — anstream's strip adapter would eat its cursor-control CSI, so it reads `AutoStream::choice` once and strips content with `anstream::adapter::strip_str`, the exact transform the buffered path applies. `NO_COLOR` now works on default `wt list`. - **One truncator** (`src/styling/line.rs`): `display::truncate_to_width` — which cut escape-blind — is merged into `truncate_visible`, which now trims trailing whitespace before the `…` and appends a reset only when the kept prefix carries an escape. Styled output is byte-identical (`ansi_cut`'s style closers both block the trim and trigger the reset); one snapshot line changes, a plain branch name losing a needless `[0m`. - **stderr joins the model**: `-v` diagnostics route through `AutoStream::auto(stderr)` (`src/logging.rs`), so `NO_COLOR` and piping reach them, and the lone `ceprintln!` (std stderr, hardcoded escapes) becomes the anstream macro (`src/main.rs`). - **The guard widens** (`tests/integration_tests/output_system_guard.rs`): the stdout scan covers all of `src/` rather than `src/commands/`, and the new `test_color_follows_the_consumer` pins six unforced-color cases — the suite's global `CLICOLOR_FORCE=1` previously made anstream's strip/pass decision invisible to every test. Behavior changes, all in the strip direction: `NO_COLOR` and piped-strip now reach progressive `wt list`, `-v` stderr diagnostics, and the clap error tips; `terminate_output`'s reset is stripped when color is off; truncation no longer leaves a `[0m` on plain text or a space before the ellipsis. The statusline's `Always` semantic is now pinned by test rather than incidental. Testing: the full pre-merge gate is green (4581 tests) plus the feature-gated PTY picker suite; `test_color_follows_the_consumer` covers the unforced matrix, and `test_list_progressive_honors_no_color` pins the progressive path on a real PTY (no SGR under `NO_COLOR`, cursor-control CSI still flowing); docs-sync verifies the generated pages byte-for-byte. The `writing-user-outputs` skill paragraph that described `println_verbatim!` now describes the `write_global()` declaration. 🤖 Generated with [Claude Code](https://claude.com/claude-code) > _This was written by Claude Code on behalf of max-sixty_ --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
00ab0ffa26 |
Canonicalize benchmark fixtures and variants (#3761)
Benchmark fixtures still encoded the benchmark that first needed each repository state, which left overlapping recipes and variants after the earlier harness consolidation. This change reduces the fixture catalog to two provenance-based bases: `Generated` builds an ordinary Git repository locally, while `Imported` copies the pinned `rust-lang/rust` corpus. Worktree, branch, and remote-ref populations remain parameters on `Generated`; prune candidates and backdrop are overlays that work with either base. The generated base deliberately combines heterogeneous worktree states, history-spread branches, and optional remote refs so ordinary list, completion, picker, first-output, alias, remove, and prune benchmarks can share it. Imported history-spread branches and clean base-tip worktrees carry their own commits, preserving the base populations without making them incidental prune candidates when overlays advance the default branch. The benchmark matrix now keeps single-factor contrasts: list scaling uses the 1- and 8-worktree endpoints; alias dispatch has a startup floor, two population endpoints, and one warm/cold variable-resolution pair; completion keeps one full-surface case; remove and prune vary cache or hook state only where the command exercises it. Historical recipes, redundant cache rows, and intermediate scaling points are removed. Manual setup paths live under `target/`, and the benchmark guide documents the resulting fixture and cache model. Tests: `cargo run -- hook pre-merge --yes` after merging current `main` (4,571 tests); targeted Criterion test-mode runs; `cargo test -p wt-perf`; benchmark check, clippy, formatting, and diff checks. > _This was written by Codex on behalf of max-sixty_ |
||
|
|
ca54dde66e |
docs(claude): size help-text prose to the behavior's share of the command (#3772)
Requested in [review feedback on
#3770](https://github.com/max-sixty/worktrunk/pull/3770#discussion_r3740664250):
> this is way too verbose; create another PR to add guidance to have the
share of the docs proportional to the share of the feature.
>
> in this case it's a tiny share of the feature and probably have zero
docs; it's implicit
The existing content principles in `docs/CLAUDE.md` govern what a piece
of help text says and where it starts, but nothing says how *much* a
behavior earns. That gap is what produced the paragraph the review was
reading: a narrow refusal in `wt remove` got a full paragraph on the
command's help page, ahead of behavior every user of the command meets.
New principle 5 makes the sizing rule explicit, including the zero case
— an edge case whose own error message states it at the moment it
matters is left implicit.
The paragraph that prompted this is already gone from #3770 (
|
||
|
|
683bc9b91b |
docs(ci): record that the release/signing environments admit any tag (#3775)
The `release` and `signing` deployment branch policies were pinned to `v*` tags; they now admit any tag, and this records that. The "Tag operations" ruleset covers `~ALL` tags, so the `v*` pattern carried no part of the gate — tend's `_tags_admin_gated` credits a tag entry on that ruleset alone and never reads the pattern. What the pattern did do was duplicate `release.yaml`'s own tag filter, which is broader: `**[0-9]+.[0-9]+.[0-9]+*` matches an unprefixed `1.2.3`, which a `v*` policy would then refuse. A release cut under that name would have stopped at `build-local-artifacts` — it names `signing` and waits only on `plan`, so the refusal lands before an artifact is built, not at a publish job. Dropping the pattern removes the only place the two could drift. This also brings the repo onto the shape install-tend's §3 recipe documents (`-f name='*' -f type=tag`), which worktrunk had deviated from. `uvx tend check` still reports 8/8. Also updates a stale `v*` reference in the `signing` job's comment in `release.yaml`. > _This was written by Claude Code on behalf of max-sixty_ --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
f3ea2fd598 |
fix(errors): report a directory holding no worktree as a directory (#3773)
Passing a path that holds no worktree used to be reported as a missing branch: ``` $ wt remove /repo/.claude/worktrees/ghost ✗ No branch named /repo/.claude/worktrees/ghost ↳ To list branches, run wt list --branches --remotes ``` The argument is plainly a path; the error calls it a branch and sends the user to a listing it could never appear in. Now: ``` $ wt remove /repo/.claude/worktrees/ghost ✗ No worktree @ /repo/.claude/worktrees/ghost ↳ The directory exists but is not a worktree; to list worktrees, run wt list ``` #3767 fixed the hook that produced the leftover directory in #3753; this fixes the message a person gets when they type such a path themselves, which judewang named there as why it was hard to diagnose. Ref #3753. Two neighbouring commands had the same cause and are also fixed. `wt switch <path>` and `wt merge <path>` used to offer `wt switch --create <path>`, which fails with `fatal: … is not a valid branch name`; and `wt config state marker set --branch <path> foo` **succeeded silently**, storing state keyed by a path string. ## How the decision is made `Repository::path_selector_error` reports a directory only when all four conditions hold. Each is here because dropping it made `wt` assert something false, reproduced against a build: | Condition | Without it | |---|---| | Git could never accept the selector as a branch name | `wt step rebase HEAD~9` reports revision syntax as a path | | A directory is there | `wt remove docs` stops meaning the branch when `docs/` sits beside it | | No worktree of this repo is registered at it | a *detached* worktree — reachable by path alone — is reported as absent, contradicting the `wt list` in the hint | | It holds no git data | a sibling repo's live checkout, or a bare repository, is called a leftover only the user can delete | That last one is the data-safety case: the new variant's hint says only the user can delete the directory, so a false positive reads as an invitation to `rm -rf` a checkout with uncommitted work. Absence of git data is *established* rather than assumed — `symlink_metadata` with only `NotFound` counting, so an unreadable clone or a dangling `.git` symlink withholds the claim instead of earning it. The first condition is git's own ref-format rules, implemented in-process and pinned against `git check-ref-format` by a test. ## Navigating the diff - `src/git/repository/worktrees.rs` — `Repository::path_selector_error` and `holds_git_data`, with the rationale for each condition recorded where it's enforced. - `src/git/repository/branch.rs` — `is_valid_branch_name`, the ref-format predicate. - `src/git/error.rs` — the new `GitError::WorktreeNotFoundAtPath` variant. - `src/git/repository/config.rs` — `target_branch_at_path` → `target_worktree_at_path`, returning the worktree whole rather than collapsing "no worktree here" with "a detached worktree here". - Call sites: `commands/remove.rs`, `commands/worktree/switch.rs`, `commands/config/state.rs`. Reporting a detached *target* made `DetachedHead`'s fixed hint wrong: `git switch` acts on the tree it runs in, so `wt merge ../B` was telling the user to switch the tree they were standing in. The variant now carries the detached worktree when that isn't the current one, and the hint becomes `git -C ../B switch <branch>` — the same reason `OperationInProgress` carries `branch`. Set at the three sites where the detached worktree isn't the current one (`require_target_branch`, `require_selected_branch`, and `wt step promote`, which raises this about the main worktree); every other site passes `None` and renders as before. 140 of the 257 added production lines are comments and docstrings — three of the four conditions look like obvious things to simplify away, so the reason each exists is written down next to it. ## Testing Unit tests at the resolver boundary cover each condition and its complement, both `directory_exists` arms, the shortcut-expansion route (`wt merge -`), detached targets, revision syntax, unresolvable `.git` entries, and bare repositories. Two integration tests drive the real binary end-to-end against `../repo.leftover` — `wt remove` and `wt switch` — covering the relative-path spelling and each command's wiring, and the `switch` snapshot pins the thing that would regress silently: that no `--create` hint is offered for a name git would reject. The ref-format predicate is verified beyond its committed table: 2,791 generated names were run against real `git check-ref-format` with zero disagreements, and two review passes did the same over their own inputs. That sweep isn't committed — it needs Python and a live `git` — so the committed table is what guards against drift. Not exercised locally: the Windows-specific behaviours are reasoned, not run — `Path::new("foo.").is_dir()` succeeding against `foo`, and drive-relative `C:foo`. CI exercises the platform but not those inputs. <details> <summary>Known gaps this would ship with</summary> - `wt switch docs/` (trailing slash, branch `docs` has a worktree, `./docs` is a source directory) answers `No worktree @ docs/`. True, unhelpful. The real fix is stripping trailing separators before resolution — a resolution change, not a message change. On Windows it reads `No worktree @ …/docs`, because `path_slash`'s `to_slash_lossy` rebuilds the string from `Path::components()` there and drops the trailing separator — still true of the path, minus the character that explains it. The doc comment on `path_selector_error` now says so rather than promising a spelling guarantee that holds on one platform. - A worktree whose registration was pruned out from under it gets a safe-but-vague message. The useful answer is a distinct `git worktree repair`-or-delete diagnosis; that's new detection, and nobody has reported hitting it. - Under `-C`, a `./`-prefixed selector renders a visible `/./` (`wt -C repo remove ./ghost` → `repo/./ghost`). Cosmetic; tidying it would discard the trailing separator the `docs/` case needs. - `require_target_branch` and `require_target_ref` remain near-copies of one ladder, and this adds a line to each. Pre-existing; extracting it is a separate refactor. - `CommandEnv::require_branch` passes `worktree: None` unconditionally, and `CommandEnv::for_selector` builds an env whose `worktree_path` is the worktree the *argument* named — so a future caller combining the two would get the unqualified hint. Nothing reaches that combination today: `merge` and `squash` are the only `require_branch` callers and both build with `for_action`, and the one `for_selector` caller (`wt step commit --branch`) never calls it. Two pre-existing bugs surfaced while reviewing this, unrelated to the diff and reproducible by branch name: `wt remove` will trash a clone that now sits at a registered worktree's path, and a recreated worktree directory leaks a raw `git rev-parse --git-dir failed (exit 128)`. </details> > _This was written by Claude Code on behalf of max-sixty_ --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
61ec8d5d2a |
docs(release): order changelog entries by reader interest and cap their length (#3774)
0.72.0's release notes ran 3,246 words across 32 entries — 101 words per bullet on average, against a skill that said "1–3 sentences". Three changes to the guidance that produced that, plus one that explains why it drifted. **Ordering sorted by change class rather than by what readers care about.** The rule ranked breaking/behavior changes first, features second, and so on, so a behavior change nobody asked for outranked a feature people requested — which is exactly what led 0.72.0. Sections stay fixed as the first ordering dimension; reader interest now decides rank inside them. Interest weighs how many readers a change reaches against how much it obliges each of them to act, so a breaking change stays near the top of its section despite reaching few people. **"1–3 sentences" was never a ceiling**, because sentences absorb whatever you put in them. It's now words: 40 per bullet, 80 for the two or three headline entries, ~1,200 for the whole release, plus a one-line `awk` command that prints per-entry counts and the average so the limit is measured rather than judged. **The mandatory verification gate only pushed one way.** Every check it ran — "understates", "changes NOT covered" — makes entries longer, and nothing made them shorter. It now flags overlong, over-internal, and misordered entries on the same footing as inaccurate ones, and asks for a shorter rewrite that keeps every user-facing claim. Review round on this PR fixed three things: the word ceiling said "section" where this file uses that word for Improved / Fixed / Documentation / Internal (it means the whole release, which is also what the measuring command spans — `Fixed` alone was 1,779 of 0.72.0's 3,246 words, so a per-subsection ceiling would barely bind); the ordering rule pointed at `/writing-prose`, which is personal config and not in this repo, so the rule is now self-contained; and ordering by audience fraction alone buried breaking changes. <details> <summary>The drift, measured across five releases</summary> | | 0.68.0 | 0.69.0 | 0.70.0 | 0.71.0 | 0.72.0 | | -------------- | ------ | ------ | ------ | ------ | ------ | | words/entry | 49 | 59 | 87 | 99 | 101 | | longest entry | 95 | 122 | 152 | 190 | 279 | Each release is drafted beside the previous section, so an abstract rule loses to a concrete neighbouring exemplar every time. The fourth change names this and says to write to the ceiling instead of to the last release. </details> A companion change adds changelogs and release notes to `/writing-prose`'s "Reference-doc conventions" scope, so its prominence rule claims this surface at all. That one is personal config and lands separately. > _This was written by Claude Code on behalf of max-sixty_ --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
d146dc5957 | skills(writing-user-outputs): route --format=json through print_json (#3750) | ||
|
|
497551e076 |
fix(nix): give the devShell every tool the test suite shells out to (#3768)
`devShells.default` listed `bash`, `zsh`, `fish` under a `# For shell integration tests` comment, but that feature drives two more shells and shells out to `jq`. So `nix develop` could not run `--features shell-integration-tests`, which is what the repo's own gate runs (`.config/wt.toml` → `cargo insta test … --all-features`). This adds `nushell`, `powershell` and `jq`. The same dependency claim was stated, wrongly, in three other places. All four now name the real set: | Where | Was | Now | |---|---|---| | `flake.nix` devShell | bash, zsh, fish | + nushell, powershell, jq | | `Cargo.toml` feature comment | bash, zsh, fish | + nu, pwsh, jq | | `tests/CLAUDE.md` | bash/zsh/fish + PTY | + nushell, pwsh, jq | | `docs/content/faq.md` | bash, zsh, fish, nushell | + pwsh, jq | `flake.nix` also referenced a `CLAUDE.md → "Shell/PTY Integration Tests"` heading that exists nowhere in the repo; it now points at the section that does. ## Verification There is no `nix` on the machine this was written on, so the two claims were checked separately rather than by entering the shell. **Is the list right?** A symlink farm modelling a devShell's `PATH` — nixpkgs stdenv's own tools plus the candidate list, and nothing else — with the full `--all-features` suite run under it. `configure_pty_command` propagates the test process's `PATH` into PTY children, so this reaches the shell tests. <details> <summary>Runs (the control is what makes the failures attributable)</summary> | PATH | Result | |---|---| | Full ambient PATH (control) | 4572 passed, 0 failed | | stdenv + every tool the suite needs | 4572 passed, 0 failed | | …minus `pwsh` | 3 `shell_powershell` tests fail | | …minus `jq` | `test_worktree_remove_hook_skips_path_holding_no_worktree` fails | | …minus `python3` / `lsof` / `ps` | 6 failed: 2 `for_each`/`post_start` (python3), 1 `remove::test_remove_reap_kills_process` (lsof), 3 pgid/process-probe (ps) | The control run matters: it establishes that every failure above is caused by the withheld tool rather than by a local flake. The last row is about what the *suite* needs, not what the shell was missing — see the correction below. </details> **Does the flake still evaluate?** `nixos/nix` in a container, evaluating `devShells.<system>.default` for all four systems `flake-utils` covers, against unmodified `main` as a control. All four evaluate, and every tool resolves on each. Not verified: nothing here was *built*, only evaluated, so a package that evaluates but fails to build would not have been caught. The nightly `nix-flake` job runs `nix flake check` on PRs touching `flake.nix`, which covers that on x86_64-linux. <details> <summary>A correction: python3/procps/lsof were never missing</summary> The first version of this PR also added `python3`, `procps` and `lsof`, claiming the shell could not run a plain `cargo test`. That was wrong, and worktrunk-bot caught it. `craneLib.devShell` sets `inputsFrom = builtins.attrValues checks ++ inputsFrom`, and `mkShell` folds each `inputsFrom` derivation's `nativeBuildInputs` into its own. `checks` includes `worktrunk-tests`, whose `nativeBuildInputs` already carry `git`, `python3`, `procps` and `lsof` — so the devShell inherited all four. Evaluating the unmodified `main` tree confirms it: its `x86_64-linux` devShell derivation already contains `python3`, `procps` and `lsof`, and contains no `nushell`, `powershell` or `jq`. The symlink farm could not have caught this: it modelled stdenv plus the literal `packages` list, so crane's inherited inputs were invisible to it by construction. The experiment established what the *suite* needs; it said nothing about what the *shell already had*. Those three lines are dropped. `git` remains listed in both places — pre-existing, and left alone here. </details> <details> <summary>A guard I added and then removed</summary> `powershell` first went in behind `lib.meta.availableOn`, because nixos-unstable's PowerShell has no `x86_64-darwin` source. Testing that guard against nixpkgs HEAD showed the actual cause: nixpkgs 26.11 dropped Intel macOS wholesale, so the entire flake fails to evaluate there regardless of the guard. On every system nixpkgs still supports, PowerShell has a build — the guard protected against nothing, and its comment justified it with the wrong mechanism. Removed; `powershell` is listed plainly with the other shells. </details> ## Notes `task setup-web` checks for `bash zsh fish nu` and not `pwsh`/`jq` — the same gap in a different environment, left alone here since its install mechanics are unrelated. > _This was written by Claude Code on behalf of max-sixty_ |
||
|
|
05a301250f |
chore(clawpatch): point two stale entrypoint symbols at the live code (#3765)
Nightly sweep finding: two `.config/clawpatch/features/*.json`
entrypoints name symbols that no longer exist.
**`feat_custom_template_expansion.json`.** `expand_command_template` was
deleted by #3635 ("derive the hook preview from the execution path"),
which folded the preview render into `render_template_preview` in
`src/commands/command_executor.rs` and left `expand_template` in
`src/config/expansion.rs` as the single runtime render. The feature file
kept pointing at the old name, so the one thing it exists to do — send a
reader to the code that renders a project-supplied template before the
approval gate sees it — lands nowhere.
- **Entrypoint** → `src/config/expansion.rs::expand_template`. The
module doc calls it "a single generic function" for template rendering,
and it's what the feature's own summary is about ("expansion happens
before the approval gate sees the final string").
- **`ownedFiles` reordered and re-reasoned.** `expansion.rs` moves first
and its reason now names the render functions. `command_executor.rs` is
added — that's where #3635 put `render_template_preview` and the
foreground step pipeline, so the file the code moved *into* was missing
from a file list that still described where it came from.
`hook_commands.rs` stays, with its reason updated to what it actually
holds now (`run_hook`, the `hook show --expanded` rows).
**`feat_custom_switch_resolve.json`.** Entrypoint `handle_switch` →
`handle_switch_command`. The function was renamed when #3049 moved
switch/remove orchestration out of `main.rs`; the definition today is
`pub fn handle_switch_command` at `src/commands/worktree/switch.rs`.
This second one is why the sweep's original audit reported only one
stale symbol: it grepped each `entrypoints[].symbol` as a plain
substring, and `handle_switch` matches inside `handle_switch_command`
(and `handle_switch_output`, `handle_switch_created_output`, …), so a
renamed symbol whose new name merely extends the old one reads as
present. Re-run anchored on a definition —
`\b(fn|struct|enum|trait|type|const|static|mod)\s+<symbol>\b` inside the
declared `path` — all 14 features now resolve, and no other entrypoint
has this shape.
Following the convention of the last edit to this directory (
|
||
|
|
2dc9380670 |
docs(ci): the tend environment-deployments gap closed with #3749 (#3764)
Nightly sweep finding: the last paragraph of "Environment protection" in `.github/CLAUDE.md` describes a gap that closed the same night it was written. #3748 added the paragraph saying the generated `tend-*.yaml` files "still carry the bare `environment: tend`" and that `tend check`'s `environment-deployments` "fails until a `uvx tend@latest init` regen lands them on tend ≥ 0.1.14". #3749 merged 3 hours later and did exactly that regen — every generated job now reads `{name: tend, deployment: false}`, and tonight's `tend check` reports `environment-deployments` as `PASS`. Left as-is, the file tells the next reader to expect a failure that no longer happens and a regen that already ran. The rewrite keeps the durable half — the generated files aren't hand-edited, because `uvx tend@latest init` overwrites them — and states the resolution instead of the pending action. <details><summary>Evidence</summary> Current state of the generated files (all eight are identical in shape): ``` $ grep -A2 'environment:' .github/workflows/tend-nightly.yaml environment: name: tend deployment: false ``` Tonight's `tend check`, run by the nightly sweep: ``` PASS environment-deployments — No job files a deployment for the 'tend' environment ``` The three checks still failing (`credential-environments`, `claude-auth`, `repo-secret-allowlist`) are tracked in #3729 and are repository-settings changes, unrelated to this file. #3760 edits the same section but not these lines, so the two don't conflict. </details> No test accompanies this — it's a documentation-only change to a file no test reads. Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> |
||
|
|
5d4d0e8407 |
docs(ci): record the model credential as environment-scoped (#3760)
Documents `CLAUDE_CODE_OAUTH_TOKEN` moving out of repo-level storage and into the `tend` environment — the last operational secret still sitting where any workflow the repo runs could read it, and the remaining `repo-secret-allowlist` failure after #3748. The environment copy is set; the repo-level copy is deleted once this PR's own `tend-review` run comes back green. That run is the verification. An environment secret outranks a repo-level one of the same name, so a job naming `environment: tend` already reads the new value while both exist — which means the credential is exercised end to end before anything is removed, rather than after. Nothing about who reads the token changes. All eight readers — `triage`, `handle`, `fix-ci`, `nightly`, `review-runs`, `notifications`, `weekly`, `review` — already declare `environment: tend`. The lead sentence of "Environment protection" claimed environments hold "credentials that grant write access". That was never the set — the model credential grants no write access to anything in this repo, and it now lives in one. It states the set directly instead, with the reason `CODECOV_TOKEN` stays outside it. The `tend` row's "Read by" cell also picks up semicolons. `every tend-*.yaml job but relay, append-gist, both create-issue-on-*-failure jobs` reads as one four-item exclusion list, which would say `append-gist` doesn't read the token — it does. > _This was written by Claude Code on behalf of max-sixty_ Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
f57fac365b |
Release v0.72.0 (#3759)
Release v0.72.0 — 55 commits since v0.71.0. Minor bump: `cargo semver-checks` reports 5 breaking library changes, so patch is disallowed pre-1.0. ## Headline changes - **`wt merge` / `wt step push` no longer autostash the target worktree** (#3703). Both strategies now advance the target through one `advance_target` — a compare-and-swap `update-ref`, then `read-tree -m -u` in the target worktree — so `refs/stash` is never entered and staged changes stay staged. - **Forge classification returns to brand-in-hostname** (#3673), reverting the exact-DNS-label rule 0.71.0 shipped. `github-enterprise.acme.com` and friends resolve again with no config. - **`[projects."…"]` keys match by `*` pattern and carry forge settings** (#3701), so one user-config entry covers every repository on a self-hosted host. - **A published JSON Schema for `wt list --format=json` schema 2** (#3747), plus machine-readable approval state and `branch_outcome` (#3710). Full detail in `CHANGELOG.md`. ## One fix made during the release cut The release audit surfaced a gap this release's own `advance_target` rewrite introduced, fixed here rather than deferred: **`wt merge` / `wt step push` now refuse a target worktree parked mid-operation.** The target sync is a two-tree merge, which refuses an unmerged index but *not* a stopped cherry-pick or rebase whose conflict has already been staged. A target paused between steps could therefore have the push range written into it, and the user's `--continue` would commit the synced tree as the step's result. The old fast-forward path got this check for free from `receive.denyCurrentBranch=updateInstead`, which refused any unclean target outright; both strategies now ask directly, and the refusal names the worktree holding the operation. `test_push_refuses_target_mid_operation` covers it in both shapes a stopped operation can take, and both are mutation-verified. With the gate disabled, the push succeeds and writes `feature.txt` into the mid-cherry-pick worktree. The rebase case was added in response to review feedback on this PR, and pins a second dependency. A rebase detaches HEAD, so `git worktree list --porcelain` reports the target with no branch and `worktree_for_branch` finds it only because `finalize_worktree` backfills from `rebase-merge/head-name`. That makes the rebase arm the one place this guarantee rests on a helper of ours rather than on git — the fast-forward path it replaced got the refusal from `find_shared_symref`. With the backfill disabled, `wt step push` succeeds against a worktree parked mid-rebase while the cherry-pick case still passes, so the gap was real. ## Validation - Local gate green: `cargo run -- hook pre-merge --yes` — 4570 tests, clippy, fmt, doc sync. - Cross-platform nightly green on the cut-from tip `3817df079` (run 31133551751): full nextest matrix on linux/macOS/Windows, feature-powerset, all three release triples, nix-flake, minimal-versions, unused-deps, crate-build, link-check. - Changelog verified entry-by-entry against the diffs by an independent pass; every one of the 55 commits either maps to an entry or is a documented skip. - `main` advanced during the CI wait. #3762 ships in this release and now has a changelog entry; the other two commits that landed (#3758, #3749) touch only `.github/`. - Data-loss surface reviewed by four independent finders over the cumulative diff. One further finding — a pre-0.72 `approvals.toml` key containing `*` being reinterpreted as a wildcard on upgrade — was reviewed and accepted as out of scope for this release. > _This was written by Claude Code on behalf of max-sixty_ --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>v0.72.0 |
||
|
|
bb580ed46b |
fix(plugin): WorktreeRemove hook skips a path holding no worktree (#3767)
## Problem The `WorktreeRemove` hook guards with `[ -e "$p" ]` — #3493's narrowing, which made the hook a no-op when the recorded worktree path is gone. #3753 reports the third state between "gone" and "live": a path that **exists but holds no worktree**. A skeleton directory left by an interrupted create or remove has no `.git`, is invisible to both `git worktree list` and `wt list`, and passes the existence guard, so `wt remove <path>` runs and resolves the path as a *branch* name: ``` ✗ No branch named /…/.claude/worktrees/<name> ↳ To list branches, run wt list --branches --remotes # exit 1 ``` An out-of-tree skeleton gives `fatal: not a git repository` instead. Claude Code reads a nonzero `WorktreeRemove` as a failed removal and keeps the session row, so the finished background session can never be deleted with Ctrl+X — the same end symptom as #3488, but permanent: prune ignores a directory that was never a worktree, so nothing heals it. ## Solution Test for git's marker rather than for the directory: ```diff - [ -e "$p" ] || exit 0 + [ -e "$p/.git" ] || exit 0 ``` Every linked worktree carries a `.git` file, and a dirty, locked, or unmerged one still does, so genuine failures keep surfacing loudly — the #2939 spirit the #3493 guard was written to preserve. Leniency stays scoped to "no worktree lives here" rather than becoming a blanket success, and the change stays inside the single-quoted `bash -c` body, so outer login-shell (fish/zsh/bash) parsing is untouched. `-e` rather than `-f` is deliberate: the *main* worktree's `.git` is a directory, so `-e` keeps `✗ The main worktree cannot be removed` loud where `-f` would silently no-op it. This composes with #3754 rather than overlapping it: the guard now runs before `-C "$p"`, so `-C` only ever resolves a path that really is a worktree. One state changes beyond the reported one, in the same direction: a registered worktree whose `.git` file was deleted by hand already failed the hook (`fatal: not a git repository`), and now exits 0, leaving a prunable registration for `git worktree prune`. Nothing `wt remove` could previously remove is skipped. ## Testing `test_worktree_remove_hook_skips_path_holding_no_worktree` runs the real command out of `hooks.json` under `bash`, feeding it the recorded path on stdin exactly as Claude Code does. It pins three directions, so neither a blanket `exit 0` nor a swallowed `wt remove` failure can satisfy it: a skeleton is a no-op and is left on disk (both in-tree and out-of-tree), a dirty worktree still fails with `uncommitted changes` and stays put, and that same worktree once clean is still removed. <details><summary>Mutation evidence and hand-verified states</summary> Each mutation was applied to `hooks.json` and the test re-run: | mutation | result | |---|---| | `[ -e "$p" ]` (the pre-fix guard) | fails — `hook must be a no-op for a path holding no worktree` | | whole body replaced with `exit 0` | fails — `hook must still refuse a dirty worktree, and for that reason` | | `wt remove … \|\| exit 0` (failure swallowed) | fails — same assertion | Hand-verified against the built binary via `WORKTRUNK_BIN`, firing the hook as Claude Code does: | state | before | after | |---|---|---| | skeleton dir, in-tree | exit 1, `No branch named …` | exit 0, directory untouched | | skeleton dir, out-of-tree | exit 1, `fatal: not a git repository` | exit 0, directory untouched | | recorded path gone | exit 0 | exit 0 | | clean worktree | removed | removed | | dirty worktree | exit 1, `has uncommitted changes` | unchanged | | unmerged branch | worktree removed, branch retained + `-D` hint | unchanged | | main worktree | exit 1, `The main worktree cannot be removed` | unchanged | | registered worktree, `.git` deleted by hand | exit 1, `fatal: not a git repository` | exit 0, prunable entry left | The test is gated on `all(unix, feature = "shell-integration-tests")`, which the required `test` jobs and the coverage run both enable; the hook parses its stdin with `jq`. **Not verified:** Claude Code's own session-row teardown — CI can't drive the agent UI. The evidence here is the hook's exit status, which is what Claude Code branches on per #3488/#3493. </details> The full pre-merge gate passes locally (4571 tests, clippy, doctests, docs sync, no pending snapshots). ## Relation to #3755 Supersedes worktrunk-bot's #3755, whose one-line hook edit is byte-identical to this one. The difference is test coverage: #3755's test passes against a hook with `|| exit 0` appended to `wt remove`, which would silently discard the dirty-worktree refusal — verified by running that test file against the mutation. This one also covers the out-of-tree skeleton. #3755's `flake.nix` addition of `jq` is left out: the devShell already omits nushell so it can't run the shell-integration suite regardless, and the nix test derivation runs default features only, where this test isn't compiled. Closes #3753. Thanks to @judewang for the report, the reproduction, and the fix direction — including the note that `git -C "$p" rev-parse --is-inside-work-tree` is not a usable test, since discovery walks up to the parent for a nested skeleton. > _This was written by Claude Code on behalf of max-sixty_ Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
5576eec1a4 |
fix(output): don't panic when a consumer stops reading stdout (#3766)
`wt list | head -3` exited 101 with `failed printing to stdout: Broken pipe`, and so did `wt list statusline | head -1` — the surface a shell prompt runs on every redraw. std's `print!`/`println!` panic on a `BrokenPipe`; anstream's drop it, which is why the `--format=json` surfaces #3746 converted already exit cleanly. Seven stdout surfaces were still on std's macros. Five of them — the `wt list` table, `--version`, `--help-md`, `--help-description`, and `wt config update --print` — are read by a person, so they go through anstream's printer, the canonical color-aware one. ## `wt list` also stops coloring a pipe `wt --help` documents `NO_COLOR` ("Disable colored output") and `CLICOLOR_FORCE` ("Force colored output even when not a TTY"). That second row only means something if color is off when stdout isn't a terminal — and `src/testing/mod.rs` sets `CLICOLOR_FORCE=1` in `STATIC_TEST_ENV_VARS` precisely so ANSI still appears in snapshots. But `wt list` wrote its escapes through std's macros, so it colored a pipe unconditionally and neither variable ever reached it. It now behaves as documented for the piped case: color on a terminal, plain on a pipe, `CLICOLOR_FORCE=1` to keep it on a pipe. `NO_COLOR` is fixed wherever `print_buffered_table` runs, which is the piped case plus `--no-progressive` on a terminal. It does **not** reach the default terminal rendering: `RenderTarget::detect` hands every tty that didn't pass `--no-progressive` to `ProgressiveTable`, which writes to a raw `std::io::stdout()` with each row's escapes already baked in. Closing that last gap means stripping SGR from the progressive rows while leaving the redraw's cursor control and the URL column's OSC 8 intact (`src/styling/hyperlink.rs` states `NO_COLOR` must not affect hyperlink support), so it's a separate change on the most visible surface in the tool. <details> <summary>PTY measurements</summary> On a terminal, with `CLICOLOR_FORCE` unset: | invocation | escapes | |---|---| | `wt list` (progressive, default) | 4590 | | `NO_COLOR=1 wt list` | 4663 — unchanged, the gap above | | `wt list --no-progressive` | 322 | | `NO_COLOR=1 wt list --no-progressive` | 0 | | `NO_COLOR=1 CLICOLOR_FORCE=1 wt list --no-progressive` | 0 — anstream checks `no_color()` first | Piped: 0 escapes by default, 17 under `CLICOLOR_FORCE=1`. Two earlier drafts of this description got this wrong in both directions, and @worktrunk-bot caught each. </details> The five changed snapshots all belong to `BareRepoTest`, whose `configure_wt_cmd` strips `CLICOLOR_FORCE` to capture "the terminal's plain output" and, until now, didn't get it. The diffs remove ANSI and change nothing else — same content, same column widths. ## Two surfaces keep the escapes For the statusline and `--help-page`, the pipe is a courier rather than the destination. A shell prompt or Claude Code captures the statusline and *renders* it; the docs pipeline converts the help page's escapes into HTML spans. Neither consumer is ever a tty, so anstream would strip them every time in production — and no test would catch it, because the suite forces color. They get `crate::output::println_verbatim!`, a sibling of `print_json` that writes the bytes through unchanged. It drops a `BrokenPipe` and still panics on any other write error, matching anstream, so both printers fail the same way on a full disk. Output is byte-identical: the help snapshots and `test_docs_are_in_sync` pass untouched. <details> <summary>Test, and two cleanups that fall out</summary> `test_stdout_surfaces_survive_a_closed_consumer` drives nine invocations across both printers with the child's stdout pipe closed before it is waited on, so the first write has no reader. That's deterministic rather than racing a consumer's exit, and `--version`'s few dozen bytes trip it as readily as the help page's 23KB — `EPIPE` is about whether a reader is attached, not about filling the buffer. Every case was checked to fail against the unfixed code. `statusline.rs` leaves `STDOUT_ALLOWED_PATHS`, since it no longer writes stdout directly; a stray `println!` there fails the guard again. `print_first_buffered_line` is gone. Its one caller is the `WORKTRUNK_FIRST_OUTPUT` benchmark hook, which wrote the same header line through a third path; a LineWriter flushes on the newline that measurement is timing, so the hook is unaffected. </details> > _This was written by Claude Code on behalf of max-sixty_ |
||
|
|
0b27755726 |
fix(help): name the doc-entry-point command from clap, not an argv scan (#3762)
Three developer entry points — `--help-page`, `--help-description`, and
`--print-schema` — each carried its own copy of a scan that picked the
subcommand out of argv by rejecting entries that looked like the binary:
```rust
.filter(|a| *a != "--help-page" && !a.starts_with('-') && !a.ends_with("/wt"))
.find(|a| !a.contains("target/") && *a != "wt")
```
Neither `ends_with("/wt")` nor `contains("target/")` matches `wt.exe`
under a backslash path, so on every Windows install all three read the
binary's own path as the command. `--print-schema` exited 2; the other
two exited 0 with empty stdout and `Unknown command: C:\...\wt.exe` on
stderr. The scan also took the value of a value-carrying global as the
command, so `wt -C <path> list --print-schema` answered `No JSON schema
for '<path>'`.
clap had already parsed the same argv one call earlier.
`parse_early_globals` runs the real `Cli` definition with
`ignore_errors(true)` and reads `matches.subcommand()` to pick the
alias-splice context, and the comment above it already said that exists
"so the splice path in `augment_help` has no separate arg scanner". The
doc entry points just never asked it. It now carries the name in an
`EarlyGlobals` struct, and the three handlers take `Option<&str>`
instead of `&[String]`.
Error messages are unchanged: `src/cli/mod.rs` has
`#[command(external_subcommand)]` for user aliases, so a name the `Cli`
definition doesn't declare still arrives intact and can be named back in
`Unknown command: X`.
`--print-schema` now prints through `crate::output::print_json` rather
than `serde_json::to_string_pretty` plus std's `println!`. std's panics
with exit 101 when a consumer closes the pipe; anstream's drops the
`BrokenPipe`. This was the last JSON-to-stdout surface still panicking
after #3746 — the schema landed via #3747 while that PR was in flight,
and it lives outside `src/commands/`, where #3746's stdout guard doesn't
scan.
<details>
<summary>Why CI never caught this</summary>
`tests/integration_tests/help.rs` and
`tests/integration_tests/readme_sync.rs` are both
`#![cfg(not(windows))]`, and on Unix the test binary is always named
`wt` under `target/`, which is exactly the shape the scan was written
against. The new test drives all four cases through a symlink named
`wt.exe` — a symlink rather than a copy because the debug binary is
67MB. Verified to fail against the pre-change code.
</details>
`wt merge --help-page` and `wt merge --help-description` still panic on
a closed pipe. Those use std's `println!`/`print!` deliberately, to
preserve the ANSI codes anstream's would strip, so they need a different
mechanism than this one. Left for a follow-up.
> _This was written by Claude Code on behalf of max-sixty_
|
||
|
|
02a8dc829c |
chore: bump taiki-e/install-action from 2.85.7 to 2.85.8 (#3758)
Bumps [taiki-e/install-action](https://github.com/taiki-e/install-action) from 2.85.7 to 2.85.8. <details> <summary>Release notes</summary> <p><em>Sourced from <a href="https://github.com/taiki-e/install-action/releases">taiki-e/install-action's releases</a>.</em></p> <blockquote> <h2>2.85.8</h2> <ul> <li> <p>Update <code>zizmor@latest</code> to 1.29.0.</p> </li> <li> <p>Update <code>typos@latest</code> to 1.49.0.</p> </li> <li> <p>Update <code>trivy@latest</code> to 0.73.0.</p> </li> <li> <p>Update <code>tombi@latest</code> to 1.2.6.</p> </li> <li> <p>Update <code>prek@latest</code> to 0.4.12.</p> </li> <li> <p>Update <code>mise@latest</code> to 2026.8.1.</p> </li> <li> <p>Update <code>convco@latest</code> to 0.7.1.</p> </li> <li> <p>Update <code>cargo-semver-checks@latest</code> to 0.50.0.</p> </li> <li> <p>Update <code>cargo-crap@latest</code> to 0.4.1.</p> </li> </ul> </blockquote> </details> <details> <summary>Changelog</summary> <p><em>Sourced from <a href="https://github.com/taiki-e/install-action/blob/main/CHANGELOG.md">taiki-e/install-action's changelog</a>.</em></p> <blockquote> <h2>[2.85.8] - 2026-08-04</h2> <ul> <li> <p>Update <code>zizmor@latest</code> to 1.29.0.</p> </li> <li> <p>Update <code>typos@latest</code> to 1.49.0.</p> </li> <li> <p>Update <code>trivy@latest</code> to 0.73.0.</p> </li> <li> <p>Update <code>tombi@latest</code> to 1.2.6.</p> </li> <li> <p>Update <code>prek@latest</code> to 0.4.12.</p> </li> <li> <p>Update <code>mise@latest</code> to 2026.8.1.</p> </li> <li> <p>Update <code>convco@latest</code> to 0.7.1.</p> </li> <li> <p>Update <code>cargo-semver-checks@latest</code> to 0.50.0.</p> </li> <li> <p>Update <code>cargo-crap@latest</code> to 0.4.1.</p> </li> </ul> </blockquote> </details> <details> <summary>Commits</summary> <ul> <li><a href="https://github.com/taiki-e/install-action/commit/cb33e69fad06166ca28a42b2575e4dadabf62ee8"><code>cb33e69</code></a> Release 2.85.8</li> <li><a href="https://github.com/taiki-e/install-action/commit/205431a517a990dfa58a0f22a02abd8cbef31c11"><code>205431a</code></a> Update <code>zizmor@latest</code> to 1.29.0</li> <li><a href="https://github.com/taiki-e/install-action/commit/e734f91b32f269a08deefc4ceb37d4aa4747a26b"><code>e734f91</code></a> Update wild manifest</li> <li><a href="https://github.com/taiki-e/install-action/commit/2596ecb10ea03dec655fc75e41a3b0f4dad7bc42"><code>2596ecb</code></a> Update <code>typos@latest</code> to 1.49.0</li> <li><a href="https://github.com/taiki-e/install-action/commit/a44271eb2d867e7b1cf13602d4e4f0a93eec2f5e"><code>a44271e</code></a> Update <code>trivy@latest</code> to 0.73.0</li> <li><a href="https://github.com/taiki-e/install-action/commit/9b100e5f66ebcc3ef6c9e7380611a923118c93bd"><code>9b100e5</code></a> Update <code>tombi@latest</code> to 1.2.6</li> <li><a href="https://github.com/taiki-e/install-action/commit/f28498427840d9dc2242c579fe43460aad78fb48"><code>f284984</code></a> Update <code>prek@latest</code> to 0.4.12</li> <li><a href="https://github.com/taiki-e/install-action/commit/3b86746a2ded65050e00646b310ebba2a15bdaf6"><code>3b86746</code></a> Update <code>mise@latest</code> to 2026.8.1</li> <li><a href="https://github.com/taiki-e/install-action/commit/9287d185066eec8a77c1f11905ac9727db063430"><code>9287d18</code></a> Update just manifest</li> <li><a href="https://github.com/taiki-e/install-action/commit/32702bef0f7f8c45b9b179686f51a0f2739378c3"><code>32702be</code></a> Update <code>convco@latest</code> to 0.7.1</li> <li>Additional commits viewable in <a href="https://github.com/taiki-e/install-action/compare/v2.85.7...v2.85.8">compare view</a></li> </ul> </details> <br /> [](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) --- <details> <summary>Dependabot commands and options</summary> <br /> You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself) </details> Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|
|
6ad0439d79 |
chore: update tend workflows (0.1.13 → 0.1.14) (#3749)
Automated nightly regeneration of tend's workflow files. **tend version:** 0.1.13 → 0.1.14 ## Notable changes - **Jobs now name the `tend` environment with `deployment: false`** (max-sixty/tend#852), so GitHub stops filing a deployment record per run and posting it on the pull request. This is what the current `tend check` flags as `environment-deployments` (#3729) — regenerating clears it, and max-sixty/tend#853 makes `check` refuse the old shape going forward. - **`id-token: write` dropped from every tend job** — a side effect of removing the claude-smoke workflow and its `tend-manual` environment (max-sixty/tend#820). None of the remaining jobs use OIDC, so the permission was unused. - **`tend-mention` counts bot engagement outside `jq`** (max-sixty/tend#840). `gh api --paginate` applies `--jq` once per page, so `| length` emitted one count per page; past 100 comments the shell variable held `100\n7`, the numeric test errored, and the bot fell through to `should_run=false` — going quiet on exactly its most-engaged threads. - **Review and triage skill fixes** — the review-record guards now ignore synthetic reply containers (max-sixty/tend#835), `/code-review` is ported into a tend-owned skill (max-sixty/tend#819), and triage substitutes the real issue number into its PR-body templates instead of leaving a placeholder (max-sixty/tend#844). - **Outage reporting is more robust** — a stranded outage row now names the trigger it points at (max-sixty/tend#823), and marking a notification read tolerates a transient run-metadata fetch failure (max-sixty/tend#843). Full compare: https://github.com/max-sixty/tend/compare/0.1.13...0.1.14 Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> |
||
|
|
3817df0795 | fix(plugin): resolve WorktreeRemove against the worktree path, not the project dir (#3754) | ||
|
|
59c7320a78 |
ci: read TEND_BOT_TOKEN from an environment in every job (#3748)
Closes worktrunk's half of the `tend` environment migration (tend's `TODO.md`, "Finish moving the operational secrets into the `tend` environment", item 2). The environment is a secret scope, not a deploy target: its deployment branch policy is what stops a workflow pushed to a feature branch from reading the bot's PAT. That gate closes only when the repo-level copy of the secret is gone, since a job naming an environment still reads repo-level secrets. worktrunk kept one because these hand-maintained workflows read `TEND_BOT_TOKEN` outside tend's generated set. ## What changed | Job | Environment | Why that one | |---|---|---| | `benchmarks.yaml` `append-gist` (new) | `tend` | `schedule`-gated, so it runs on `main` | | `benchmarks.yaml` `create-issue-on-benchmark-failure` | `tend` | already `schedule`-gated | | `nightly.yaml` `create-issue-on-nightly-failure` | `tend` | already `schedule`-gated | | `release.yaml` `publish-winget` | `release` | runs on a `v*` tag push | | `release.yaml` `publish-homebrew` | `release` | runs on a `v*` tag push | Every job reading `TEND_BOT_TOKEN` now names an environment, so deleting the repo-level copy breaks nothing. ### The gist append moved into its own job The `benchmarks` job has no `if` gate, so putting `tend` on it would refuse a `workflow_dispatch` against a non-`main` ref — on-demand runs against a chosen branch are what that trigger is documented for. A job GitHub skips never requests its environment, so moving the append into a `schedule`-gated `append-gist` job keeps the policy off the dispatch path entirely. It reads `target/criterion` back from the artifact the `benchmarks` job already uploads. `create-issue-on-benchmark-failure` now `needs` both jobs, so a failed append still files an issue — previously it failed the `benchmarks` job directly. ### Why the release jobs get `release`, not `tend` A tag push is not bot-steerable and tag creation and update are already restricted to admins by the "Tag operations" ruleset, so a tag policy is a real boundary. The tag entry cannot go on `tend`: `tend check`'s `check_environment` pins that policy to exactly the protected branches and its `--fix` deletes anything else. `release` already exists with a `v*` tag policy and already holds `AUR_SSH_PRIVATE_KEY`, so no new environment and no new credential — `TEND_BOT_TOKEN` is seeded into it as a second copy. ### `deployment: false` Jobs naming `tend` use the mapping form. GitHub files a deployment record for every job that names an environment, against whatever ref the run belongs to; under `pull_request_target` that is the PR's own head, which is why PR timelines grew a "worktrunk-bot deployed to tend" line on every push. `deployment: false` drops the record and keeps the gate. The release jobs keep their records, which land in no PR timeline. ## Follow-up: one step, after merge `TEND_BOT_TOKEN` is **already seeded into the `release` environment** (read from the local `worktrunk-bot` gh config dir at `~/.config/gh-bots/worktrunk-bot`, so no new credential was minted and nothing was pasted). Verified: the token resolves to `worktrunk-bot` and has push on both `max-sixty/winget-pkgs` and `max-sixty/homebrew-worktrunk`. That leaves one step, and it must come **after** this PR merges: ``` gh secret delete TEND_BOT_TOKEN --repo max-sixty/worktrunk ``` Not before. On `main` today the gist append, both `create-issue-on-*-failure` jobs, and the two publish jobs still read the token with no environment named, so deleting the repo-level copy first would break the next benchmarks cron (03:47 UTC daily). Merging this PR is what makes the deletion safe. ## What this does not fix - `repo-secret-allowlist` still fails on `CLAUDE_CODE_OAUTH_TOKEN`, also at repo level. It cannot be read back either; separate item. - `environment-deployments` still fails on the generated `tend-*.yaml`, which pin tend 0.1.13 and carry the bare `environment: tend`. Those are regenerated by the published tend, and a regen also carries an unrelated `gh api --paginate` fix in `tend-mention.yaml`, so it belongs in its own PR. ## Verification - The `append-gist` script was run end-to-end against a real `benchmark-results-*` artifact from run 30976221483. It emits 40 rows whose `bench` names match the live gist's existing rows exactly, confirming the artifact round-trip preserves paths relative to `target/criterion`. - That a skipped `if` short-circuits the environment gate is confirmed by run 31066000517: `publish-cargo`, which names `environment: release`, completed as *skipped* on a `pull_request` from a non-tag ref rather than failing on the policy. - `actionlint` reports the same eight pre-existing shellcheck notes as `main`; no new findings. `pre-commit` passes. > _This was written by Claude Code on behalf of max-sixty_ |
||
|
|
860fa02ecf |
fix(output): don't panic when a --format=json consumer closes the pipe (#3746)
`print_json` — the pretty-JSON printer behind `--format=json` — lived in
`src/commands/list/mod.rs`, under the one command that happened to need
it first, while `wt list statusline` and `wt config approvals list`
reached across for it. It now lives in `src/output/json.rs`, re-exported
as `crate::output::print_json`.
Moving it surfaced thirty other call sites that had each open-coded the
same two lines, and that those two lines were not equivalent everywhere.
Four printed through anstream's `println!` (via `worktrunk::styling`),
the other twenty-six through std's. anstream drops a `BrokenPipe` write
error; std panics. So on main today:
```console
$ wt config state get --format=json | head -3
{
"ci_status": [
{
thread 'main' panicked at library/std/src/io/stdio.rs:1165:9:
failed printing to stdout: Broken pipe (os error 32)
```
while `wt config show --format=json | head -3` exits cleanly — same
command family, different behavior, decided by which macro happened to
be imported in that file. `EPIPE` depends on whether a reader is still
attached, not on how much is written, so this is a race rather than a
size threshold: an output that fits the pipe buffer usually lands before
the consumer goes away, which is why it stayed hidden rather than why it
is safe. A three-byte write panics just as readily once the reader has
closed.
All thirty-six call sites now go through `print_json`, and `print_json`
prints through anstream, so no `--format=json` surface panics on a
closed pipe. The command above now exits 0 with an empty stderr; also
checked by hand on `wt list`, `wt step prune --dry-run`, `wt config
show`, `wt step eval`, `wt config approvals list`, and `wt list
statusline`.
`wt switch --format=json` is the one surface that isn't a `print_json`
caller: it emits its single result as one compact line, which is what a
shell loop reads, so converting it would change a machine-readable
format for no gain. It gets the same anstream printer instead, which is
the part that matters here.
`wt list statusline --format=json` had a third instance hiding in its
schema-1 empty path, a bare `println!("[]")` on std's macro — on the
surface that runs on every prompt redraw. It now serializes an empty
array through `print_json`, which emits the same two bytes;
`test_statusline_json_outside_worktree` already asserted them. A
module-level `println` import would have been wrong there, since the
text statusline's `println!` deliberately bypasses anstream so its
pre-rendered ANSI survives a non-tty stdout.
Pruned eight now-dead entries from `STDOUT_ALLOWED_PATHS` in
`tests/integration_tests/output_system_guard.rs`. Centralizing these
writes left seven allowlisted files with no direct `println!` at all, so
their exemptions covered no code and the guard quietly stopped
protecting exactly the files this branch touched; a stray `println!` in
`merge.rs` or `remove.rs` fails the test again. (`alias.rs`'s entry was
already dead.)
Serialization is otherwise byte-identical: serde escapes control
characters, so raw ANSI never reaches this output and anstream's
stripping is a no-op on it.
`src/commands/CLAUDE.md` gains one line under "Adding a CLI Command"
stating the rule — stdout through anstream, `print_json` as the printer,
switch's compact line as the one shape difference — so the next JSON
surface doesn't open-code it again.
Two `print_json` calls the diff rewrote were uncovered on the base
commit, which pulled their misses into `codecov/patch` without any
behavior changing. Rather than hand over a waiver, both now have tests:
`wt remove --format=json` with no branch argument (the single-worktree
path, distinct from the named removals already tested) and `wt step
for-each --format=json` interrupted mid-loop, which still owes its
consumer the results collected before the signal. Each was
mutation-checked — deleting the line under test fails it — because a
one-item array is what both the abort and completion paths produce, so a
weaker assertion would pass either way.
> _This was written by Claude Code on behalf of max-sixty_
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
7f8ac8e5d7 |
feat(list): publish a JSON Schema for the schema-2 envelope (#3747)
`wt list --format=json` schema 2 now has a published, machine-readable contract at [worktrunk.dev/schema/list-v2.json](https://worktrunk.dev/schema/list-v2.json). The schema was already derived — `test_schema_generates` built one, asserted it compiled, and threw it away, with a comment saying "until the schema export ships." This ships it: `wt list --print-schema` prints the document (a developer entry point alongside `--help-page`, intercepted before clap), and a new step in `test_docs_are_in_sync` commits it to `docs/static/schema/list-v2.json`, the same generate-and-commit pattern as `llms.txt`. It shells out rather than calling `schema_for!` because `JsonEnvelope` lives in the bin-only `crate::commands` tree. Two things had to be fixed for the document to be usable. **The contract.** `schema_for!` generates under schemars' *deserialize* contract, which marks a `skip_serializing_if` field required — nothing supplies it on the way in. The first document I generated therefore required `default_branch`, `upstream`, `pr`, `checks`, `summary` and `vars` on every item, all of which the absence rule routinely omits, so it rejected the output it documents. Generating under `for_serialize()` fixes it. **The vocabularies.** Four fields — `checks.status`, `display.state`, `default_branch.integration.reason` and `worktree.operation` — were `&'static str`, so the schema described them as bare strings. They are now `JsonCheckStatus`, `JsonMainState`, `JsonIntegrationReason` and `JsonOperation`, each converted from its domain enum by an exhaustive match, so a new `CiStatus`, `MainState`, `IntegrationReason` or `InProgressOperation` variant is a compile error rather than a value silently missing from the published vocabulary. **The emitted JSON is unchanged**; the existing envelope snapshot passes untouched. <details> <summary>Before and after, for one item</summary> ```json // before — rejects its own output, and loses the vocabulary "required": ["default_branch", "upstream", "pr", "checks", "summary", "vars", "display"], "status": { "type": "string" } // after "required": ["branch", "head", "display"], "status": { "enum": ["passed", "running", "failed"] } ``` </details> ## Testing `test_schema_accepts_envelopes` validates a battery — every `CiStatus` over both sources, every `MainState`, a populated worktree row, an integrated row with an upstream and a dev server, plus the absent and null arms of the absence rule — against the same document `--print-schema` emits. Validating proves nothing about a type the battery never instantiates, so the test also pins every non-`Nullable_` type in the document to a path that must carry a non-null value. A new `Json*` type fails until the battery reaches it, and a row that stops populating one fails too — the check reports the type names rather than leaving the gap to a reader. This needed a `jsonschema` dev-dependency: schemars only generates, and derives the document from the types without ever seeing an envelope, so nothing otherwise tied the two together. The test was confirmed to fail on the bug it exists for. Reverting to `for_deserialize()` makes it report `pr`, `checks`, `summary`, `vars` and `display.columns` as wrongly required. The dependency is dev-only: `reqwest`, `rustls` and `async-trait` stay unselected so no HTTP stack comes along, and `cargo tree --package worktrunk --edges normal -i jsonschema` finds no path to it. One direction it deliberately does not cover: a *loosening*. If a field reverted to `&'static str` the schema would say `type: string` and anything would validate. That direction is held by the compiler instead, via the exhaustive matches. ## Notes for review - `schema_document()` lives in `json_v2.rs` beside the types, not in `help.rs`, so `--print-schema` and the test compile the same document rather than two constructions that could drift on the contract setting. - The lychee exclusion for `worktrunk.dev/schema/` follows the entry directly above it: a generated link that 404s until the site deploys. > _This was written by Claude Code on behalf of max-sixty_ |
||
|
|
bdd7113c95 | fix(shell): catch a failing --execute body in the nushell wrapper (#3734) | ||
|
|
78010166bd |
fix(shell): bypass rm aliases in the nushell wrapper's cleanup (#3732)
## Problem The nushell wrapper's temp-file cleanup called a bare `rm -f`. Nushell resolves aliases at parse time, and `config.nu` runs before the `$nu.vendor-autoload-dirs` file the wrapper installs into — so a user's `alias rm = ...` is already in scope when the wrapper's `def` is parsed, and intercepts the cleanup. #3714 fixed the same shadowing in the bash and zsh wrappers with `command rm`; nushell has no `command` builtin, so it was left out. Two failures, both verified end-to-end against nushell 0.114.1 (the version CI pins) driving the real rendered wrapper: | `alias rm =` | before | after | |---|---|---| | *(none)* | exit 0, stdout returned, 0 temp files left | unchanged | | `^false` | exit 1, **stdout empty**, **3 temp files leaked** | exit 0, stdout returned, 0 left | | `^echo TRASHED` | `TRASHED -f /tmp/...` lines **injected into stdout**, **3 temp files leaked** | exit 0, stdout returned, 0 left | The `^false` row is the serious one. Cleanup sits between `let output = (open $stdout_file --raw)` and the function's return, and nushell 0.98+ raises `ShellError` on a non-zero external exit — so an `rm` alias that fails, prompts, or isn't installed on that machine aborts the wrapper before it returns, and the command's stdout is silently discarded. Only stdout is affected; stderr has already streamed to the terminal, which is why the symptom hides on `wt switch` (its output is on stderr) and shows on `wt config show`. ## Solution Consolidate the two cleanup calls into one, after the last read of any temp file, and branch at runtime on `$nu.os-info.family`: ```nu if $nu.os-info.family == "windows" { try { rm -f $cd_file $exec_file $stdout_file } } else { try { ^rm -f $cd_file $exec_file $stdout_file } } ``` `^rm` bypasses alias expansion entirely — the Unix fix is complete, not partial. Windows keeps the builtin because `^rm` is external-only and Windows has no external `rm`; `try` covers both branches so cleanup can never abort the wrapper again. Branching at *runtime* rather than forking the template by build platform is what keeps this cheap: `wt config shell init nu` renders one text on every platform, so there's a single `init_nu` snapshot and no platform-specific rendering to maintain. <details><summary>Options considered and rejected</summary> - **`` `rm` `` (backtick-quoted)** — bypasses the alias, but `` `rm` `` with `PATH` emptied reports ``Command `rm` not found``, and `` `rm` `` with no args prints `/usr/bin/rm: missing operand`. It resolves to the *external*, so it's just `^rm` spelled differently — same Windows gap, no benefit. - **`hide rm`** — works inside the `def`, but `hide` is a parse-time keyword that leaks into the enclosing scope. For an autoloaded file that scope is the user's session, so installing shell integration would silently unbind their own `rm` alias. Same leak inside a `do {}` closure. Worse than the bug. - **`try` alone, no `^rm`** — cross-platform and kills the abort, but leaves the alias running: a `trash` alias still trashes worktrunk's temp files on every invocation, and an `rm -i`-style alias still prompts. - **Forking the template by build platform** — same end state on each platform, at the cost of platform-specific `init_nu` snapshots. The runtime branch gets there without them. </details> ## Testing `test_nu_wrapper_cleanup_survives_rm_alias` in `tests/integration_tests/shell_wrapper.rs` runs the real wrapper through a PTY with `alias rm = ^false` declared ahead of it, against a dedicated `TMPDIR`, and asserts both symptoms: the wrapper's stdout comes back non-empty, and the temp dir is empty afterwards. It fails on the current template (marker absent — the wrapper aborted) and passes with the fix. Also run locally: `cargo test --lib --bins`, and the full `shell_wrapper` + `config_show` + `test_docs_are_in_sync` integration set with `--features shell-integration-tests` (201 passed). ## Not fixed here A user alias to a *custom command or builtin* whose signature rejects `-f` — e.g. `alias rm = print "…"` — makes the whole wrapper file fail to parse, leaving `wt` undefined rather than merely broken. That's pre-existing (today's template has two bare `rm -f` calls) and survives this change, because the Windows branch is still parsed on Unix even though it never runs. Eliminating it means emitting no bare `rm` at all on Unix, which is the template fork above. Flagged on #3730 rather than bundled in here. --- Closes #3730 — automated triage Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> |
||
|
|
970976bd32 |
Consolidate benchmark recipes and cases (#3721)
Benchmark fixtures had accumulated around individual call sites, leaving the same repository shapes and command modes expressed several ways. This change makes repository state the organizing concept: benchmark groups select semantic `FixtureRecipe`s, share table-driven cases, and retain separate fixtures only when a controlled contrast, destructive precondition, or disproportionate setup cost requires one. The real-repository list benchmarks now share one pinned `rust-lang/rust` fixture with eight worktrees and fifty branches spread across history. The list matrix keeps default, branch, warm, and cold coverage without maintaining several “real” repository handles. Remove and prune cases share the same case machinery, while the destructive large-repository prune state remains separate. The scheduled workflow now converts Criterion estimates directly with `jq`, removing the one-off Python converter and its tests. The benchmark guide records the canonical-fixture principle and the remaining recipe-to-group mapping. Tests: `cargo run -- hook pre-merge --yes` (4,551 tests); `cargo bench --bench list large_repository -- --test`; `cargo test -p wt-perf`; benchmark check, clippy, formatting, and diff checks. > _This was written by Codex on behalf of max-sixty_. |
||
|
|
2203c414f5 |
fix(docs): convert a config-example link whose text holds a bracketed span (#3731)
`wt config create --project` writes a comment into the user's `.config/wt.toml` — and `wt config create --help` prints the same text — carrying a raw, unresolvable Zola link: ``` # When many repositories share one self-hosted host, name it once in user config with a [pattern-keyed `[projects]` entry](@/config.md#user-project-specific-settings) instead of repeating this block in each repo. ``` Every other cross-reference in that file is a plain URL (`… see \`wt hook\` (https://worktrunk.dev/hook/) …`), because `transform_config_source_to_toml` converts the `after_long_help` markdown to plain text on the way into `dev/wt.example.toml`. This one link isn't converted: `convert_markdown_links_for_config` matched link text with `[^\]]+`, which stops at the first `]` — here the one closing the nested `` `[projects]` `` span — so the regex failed to match and the markdown survived verbatim. The line arrived with #3701; it's the only link in either generated example file with a bracketed span in its text. ## The fix **One rule for `]` in link text.** `ZOLA_LINK_PATTERN`, earlier in the same file, already solves this problem for the skill mirrors — it alternates a backticked code span with any non-`]`-non-backtick char, which is why `skills/worktrunk/reference/config.md` renders this very sentence with a resolved URL while the TOML example didn't. `convert_markdown_links_for_config` now uses that same class rather than a second, weaker one. Brackets in these link texts always sit inside a code span, so the class fits the shape exactly, and it covers `[[…]]` array-of-tables names as well — these sections already document `[[projects."…".post-start]]` pipelines, so a link naming one is the next form to arrive. Regenerating produces the intended form: ``` # When many repositories share one self-hosted host, name it once in user config with a pattern-keyed `[projects]` entry (https://worktrunk.dev/config/#user-project-specific-settings) instead of repeating this block in each repo. ``` **A shape the regex declines now fails loudly.** Widening the class fixes the shapes we know about; it can't fix the next one. `finalize_skill_content` already handled that risk with a guardrail — after the rewrite it scans for a stray `](@/…md` and panics with the offending line, precisely because "the regex declined on an unexpected character in the link text" is the expected failure mode. `transform_config_source_to_toml` had no equivalent, which is why this one reached `dev/wt.example.toml` and the `--help` output. That check is now extracted into `assert_no_untransformed_zola_links` and called from both surfaces, so the next unsupported shape is a test failure naming the line rather than a raw `@/config.md` target in a user's config file. ## Why nothing caught it `test_project_config_source_generates_example_toml` compares `dev/wt.example.toml` against the output of this same transform, so an unconverted link is "in sync" by construction — the sync test can't see the difference between a link that converted and one the regex declined to match. Two tests close that gap: - `test_config_markdown_links_convert_to_plain_text` asserts the transform's output directly. It fails on `main`'s regex with exactly the reported symptom, and pins the forms already working (Zola page, Zola page + anchor, absolute URL, two links on one line, the `[[…]]` array-of-tables name) plus the case that must *not* convert — a bare `` `[forge]` `` span is not a link and has to survive verbatim. - `test_untransformed_zola_link_fails_the_config_transform` covers the backstop itself: an unbalanced backtick in link text makes the rewrite decline, and the assertion turns that into a panic naming the line. ## Files - `tests/integration_tests/readme_sync.rs` — the shared link-text class, the guardrail extraction and its second call site, and both tests. - `dev/wt.example.toml` — regenerated by the sync test (one line). - `tests/snapshots/…help_config_create.snap` — the same line, as `wt config create --help` renders it. Ran locally on the final state: `readme_sync::` (15), `test_help` (47), `cargo clippy --tests --all-features`, and `cargo fmt --check`. All green; the generated files are byte-identical under the new class, so the sync tests pass without regenerating. The full gate runs in CI. --------- Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> |