mirror of
https://github.com/max-sixty/worktrunk.git
synced 2026-09-14 20:00:38 +08:00
codex/remove-codex-cloud-specific-tests
291 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
350ad764ef |
skills(running-tend): fix the statusline cache-check recipe for Linux (#3825)
The `running-tend` skill's weekly statusline cache-check recipe can't run as written: its `sed -i ''` is BSD syntax, and the weekly job runs on `ubuntu-24.04`. GNU sed reads the `''` as the script and the substitution expression as a filename, so the step exits 2 and the input file keeps its `REPLACE_WITH_CWD` placeholder — a session that pushed past the error would then profile a statusline render against a nonexistent working directory. Building the JSON with `jq -n --arg` drops the placeholder dance entirely and can't be corrupted by a path containing a quote. The same block also invoked `wt list statusline --claude-code`, which has been deprecated in favour of `--format=claude-code` since #2436 and warns on every use. <details><summary>Verification</summary> GNU sed 4.9 on this runner, reproducing the failure: ``` $ sed -i '' "s|REPLACE_WITH_CWD|/home/x|" sedtest.txt sed: can't read s|REPLACE_WITH_CWD|/home/x|: No such file or directory exit=2 ``` The corrected recipe, run end to end against a debug build of this checkout: ``` $ jq -n --arg cwd "$PWD" '{hook_event_name:"Status", workspace:{current_dir:$cwd}, model:{display_name:"Opus"}, context_window:{used_percentage:42.0}}' > /tmp/statusline-input.json $ wt -vv list statusline --format=claude-code < /tmp/statusline-input.json > /dev/null exit=0 $ wt config state logs profile --format=json | jq .cache {"same_context_duplicates": [...], "same_context_extra_calls": 2, ...} ``` The field names match what `StatuslineInput` reads — `/workspace/current_dir`, `/model/display_name`, `/context_window/used_percentage` in `src/commands/statusline.rs`. Deprecation warning on the old flag: ``` $ wt list statusline --claude-code < /tmp/statusline-input.json ▲ --claude-code is deprecated; use --format=claude-code instead ``` No regression test: the change is to a skill markdown file, which has no test harness in this repo. The recipe itself was run end to end instead, as above. </details> Found by the nightly survey (`.claude/skills/` is not on the rotation, but the statusline recipe surfaced while checking `wt list statusline` doc accuracy in `skills/worktrunk/reference/claude-code.md`). --------- Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> |
||
|
|
18b408ce98 |
Add shared Codex Cloud environment setup (#3810)
## Summary - add a repository-owned Codex Cloud Taskfile, exposed through root setup and maintenance tasks - share setup and maintenance preparation in one task instead of two scripts - document concise, checksum-gated environment commands - preserve the proven UID 1000, `tini`, pinned-tool, and retry behavior - keep tool pins, archive checksums, Task version, and launcher digests synchronized by test and maintenance guidance ## Why Worktrunk's full suite needs dependencies and process/permission semantics beyond the stock universal image. The working configuration previously lived only in one saved environment, where other contributors could neither review nor reuse it. The dedicated Taskfile sits beside the project Taskfile without making unrelated task edits invalidate the Cloud environment hash. Root wrapper tasks launch it as a new Task process so its repository-relative paths retain their own Taskfile context. ## Security Codex checks out the task branch before setup or maintenance. Each saved launcher verifies the dedicated Taskfile's fixed SHA-256 digest before executing it as root. `MISE_NO_CONFIG=1` also prevents branch-controlled mise configuration from running before the verified Taskfile. Approved changes require updating the digest in environment settings, which invalidates the cache. Repository-sensitive Rustup, pre-commit, and Cargo work runs as the image's UID 1000 `ubuntu` user; root is limited to the verified system and ownership preparation. ## Validation - root and direct Taskfile discovery expose `setup-codex` and `maintain-codex` - YAML parsing and extracted Bash syntax pass - warning-level ShellCheck passes - applicable pre-commit hooks pass - positive and negative checksum checks pass - the launcher-sync integration test passes - independent adversarial, abstraction-level, and current-head code reviews are clean - exact Taskfile validation passed in Codex Cloud task `task_e_6a7e53a87e908325bf50ea3413ed521c` - setup and maintenance launchers passed - root and direct Task discovery passed - Cargo identity probe returned UID 1000 - `cargo run -- hook pre-merge --yes` exited 0 - 4,601/4,601 tests passed; all gate components passed - HEAD remained `288953dcb18466f64b8e5355192ae297f4862240` and the checkout remained clean - final-head Cloud task `task_e_6a8089197ecc8325afd723d41beb5c50` reached `READY` with no diff after about 38 minutes on `dab4a5dcd5f343c3e5ea0a1183e9fcc5d8271a08`; its transcript was unavailable, so no finer-grained result is claimed - all 18 applicable final-head checks pass on Linux, macOS, and Windows, including coverage, `codecov/patch`, and current-head tend review > _This was written by Codex on behalf of @max-sixty_ |
||
|
|
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
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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) | ||
|
|
60830520c5 |
skills(running-tend): move outage recovery upstream into tend (#3742)
Removes the Outage Recovery section from the overlay; the guidance now lives in tend's bundled `review-runs` skill, proposed in [max-sixty/tend#851](https://github.com/max-sixty/tend/pull/851). **Why upstream.** Nothing in the section was worktrunk-specific: the `tend-outage` issue shape, the `pull_request_target`-only trigger that makes a dead `tend-review` unrecoverable, the session-log message that names the quota limit, and the scheduled-vs-event-triggered split are all properties of tend itself. Every consumer running `tend-review` loses reviews the same way. That was already the disposition in [max-sixty/tend#816](https://github.com/max-sixty/tend/issues/816) (Gap 2), which the upstream PR closes out. **What changed from the original PR.** This branch first carried a three-line correction naming the second subscription limit. That correction is folded into the upstream section instead, so the net diff here is the removal. **Sequencing.** Merging this before #851 lands leaves a window with no drain guidance in either place. The window costs one daily sweep at most, and the outage issue accumulates rather than losing data, so it's a maintainer call whether to hold this until upstream merges. --------- Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> |
||
|
|
6666ac6eed |
skills(running-tend): check --config-set before calling a setting unpinnable (#3736)
Daily `review-runs` sweep of the 2026-08-03/04 window. One maintainer correction in the window is generalizable repo knowledge, so it gets codified here. ## What happened Triaging [#3696](https://github.com/max-sixty/worktrunk/issues/3696) ("Allow selecting the `wt list` JSON schema on the command line"), the bot read [`resolve_json_schema`](https://github.com/max-sixty/worktrunk/blob/286004f1ebe5f6563c772be24987394144a29215/src/commands/list/mod.rs#L220), saw it reads `repo.config().list.json_schema`, and [posted](https://github.com/max-sixty/worktrunk/issues/3696#issuecomment-5153609243) that "there's no way to pin it per-invocation independent of that config" — then went further and said the usual alias deflection "can't substitute, since there's no composable way to select the schema without this flag existing first". max-sixty [corrected it](https://github.com/max-sixty/worktrunk/issues/3696#issuecomment-5159480766): "the need is real, but it's already met, so I'm not going to add the flag" — `wt --config-set 'list.json-schema = 2' list --format=json` pins it per invocation, and `WORKTRUNK_LIST__JSON_SCHEMA` does the same a layer down. The proposed `--schema` flag was declined as "a third spelling of the same thing on a surface I deliberately keep small". The issue was closed on that basis. `--config-set` has been documented since [#3138](https://github.com/max-sixty/worktrunk/pull/3138) (2026-06-19), in `after_long_help` in [`src/cli/mod.rs`](https://github.com/max-sixty/worktrunk/blob/286004f1ebe5f6563c772be24987394144a29215/src/cli/mod.rs#L2558) and its rendered mirror. It was available to read; the triage just never looked above the resolver. ## Why codify it rather than log it The specific miss is what the gates would call a single occurrence, but the shape isn't a behavioural nudge that risks noise elsewhere — it's a factual property of worktrunk's config layering, and the failure mode it prevents is a **publicly posted false capability claim**, which is the class `running-in-ci` treats as the most corrosive output failure. It's also invisible: nothing fails, the reply just reads as authoritative and wrong until a maintainer happens to catch it. It sits directly above **Suggesting Aliases for Niche Feature Requests** because it serves the same flag-growth limit that section already encodes, and is the cheaper answer when the ask is "select an existing config value per invocation" — the alias deflection is the fallback when it isn't. <details><summary>Window summary</summary> 46 token-bearing runs, **0 failed**, 0 near-timeout (longest job 68 min against the 360-min cap). 3 bot PRs opened ([#3731](https://github.com/max-sixty/worktrunk/pull/3731), [#3732](https://github.com/max-sixty/worktrunk/pull/3732), [#3734](https://github.com/max-sixty/worktrunk/pull/3734)), 2 merged ([#3722](https://github.com/max-sixty/worktrunk/pull/3722), [#3726](https://github.com/max-sixty/worktrunk/pull/3726)), 0 closed without merge. [#3721](https://github.com/max-sixty/worktrunk/pull/3721) — the PR stranded by the 08-03 outage — was reviewed and approved, confirming yesterday's recovery worked. Full evidence in the tracking issue [#3691](https://github.com/max-sixty/worktrunk/issues/3691). </details> --------- Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> |
||
|
|
04bdfac687 |
skills(running-tend): re-run event triggers stranded by a failed session (#3722)
Daily review of the 2026-08-02T08:51Z → 2026-08-03T08:51Z tend window. 8
runs failed, all with the same root cause, and one of them left a PR
permanently un-reviewed. This adds the recovery step that was missing.
## What happened
Every failure in the window was `claude -p exited non-zero (exit=1)` at
the harness step, across three workflows — `tend-notifications` ×5,
`tend-review` ×2, `tend-nightly` ×1. The session logs give one cause for
all eight:
```
{"model":"<synthetic>","stop":"stop_sequence",
"content":["You've hit your session limit · resets 8:30am (UTC)"]}
```
Two 5-hour subscription windows were exhausted (resets at 03:10Z and
08:30Z). That is quota, not a bug — no fix PR is warranted for the
exhaustion itself.
The consequence is the problem. The harness recorded all eight in the
`tend-outage` issue
[#3715](https://github.com/max-sixty/worktrunk/issues/3715), one row
each with the run link and the triggering `#N`, and then nothing re-ran
them. `tend-review` fires only on `pull_request_target`, so a review
that dies at turn 1 never retries.
[#3712](https://github.com/max-sixty/worktrunk/pull/3712) recovered by
luck — a later push re-triggered it.
[#3721](https://github.com/max-sixty/worktrunk/pull/3721) did not: at
analysis time it was the only non-bot open PR in the repo with zero
`worktrunk-bot` reviews, its head unchanged since 06:06Z, and nothing
was going to re-trigger it. I re-ran
[30789110268](https://github.com/max-sixty/worktrunk/actions/runs/30789110268)
manually (attempt 2) to unblock it.
## Why codify it
This isn't a one-off. "Bot temporarily unavailable" issues recur about
weekly — [#3715](https://github.com/max-sixty/worktrunk/issues/3715),
[#3632](https://github.com/max-sixty/worktrunk/issues/3632),
[#3586](https://github.com/max-sixty/worktrunk/issues/3586),
[#3260](https://github.com/max-sixty/worktrunk/issues/3260),
[#3153](https://github.com/max-sixty/worktrunk/issues/3153), and ~15
more back to April. The failure is structural: a rate-limited run always
dies at turn 1, always leaves its trigger unserviced, and there is no
decision point where the bot could have done better. It's also invisible
— the PR just sits there looking reviewed-by-nobody, and the last two
review windows both recorded "0 failed runs", so the log gave no warning
either.
## The change
One section in `running-tend`, placed next to the existing CI-fix
classification guidance:
- Drain the open `tend-outage` issue during the daily `review-runs`
sweep and extract the stranded run IDs and triggers.
- Re-run only event-triggered runs — `tend-nightly` and
`tend-notifications` recover on their next cron tick.
- Check the PR still lacks the work before re-running, so a trigger that
already recovered via a later push doesn't burn quota twice.
- Diagnose the cause from the session log rather than the issue body,
which says only "The bot failed to process a request".
Both recipes were run against this window's data before being written
down: the outage-issue drain returns all 8 run IDs plus `#3712` /
`#3721`, and the session-log `jq` returns the session-limit line from
the downloaded artifact.
<details><summary>Window detail</summary>
**Spend**: $49.74 API-list across 52 token-bearing runs (7.7K in /
304.6K out / 2.5M cache-create / 31.2M cache-read). Per workflow:
`tend-review` $35.16 (29 runs, 15 distinct PRs), `tend-weekly` $4.64
(1), `tend-notifications` $4.34 (10), `tend-mention` $5.60 (11). Down
from $86.04 the prior window, partly because eight runs died at turn 1
for $0. No waste outlier — the review spend tracks real PR throughput.
**Outcomes**: 3 bot PRs merged
([#3706](https://github.com/max-sixty/worktrunk/pull/3706),
[#3707](https://github.com/max-sixty/worktrunk/pull/3707),
[#3708](https://github.com/max-sixty/worktrunk/pull/3708)), 0 closed
without merge, 0 maintainer corrective comments. `tend-nightly`
[30793479820](https://github.com/max-sixty/worktrunk/actions/runs/30793479820)
died at turn 1, so this window's nightly sweep produced nothing — it
recovers on tomorrow's cron. No job ran long (max 4 min among failures;
nothing near the 360-min cap).
**Notifications cadence checked, not a problem**: `*/15` gives ~36
runs/day, but only 10 reached the harness — the workflow's pre-Claude
notification gate exits the rest before any token spend. ~6 substantive
runs/day at ~$0.87. Not the quota driver.
</details>
---------
Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
|
||
|
|
deac6fc19d |
docs(release): spell out the merge step instead of naming a personal skill (#3674)
Step 11 of the release skill pointed at `/gpk`, a user-level skill that
isn't in this repo. `.claude/skills/` is checked in, so the one step a
contributor can't infer was written as a command they don't have.
(Nothing else leaks: the shipped plugin skills reference only their own
`/worktrunk` and `/wt-switch-create` plus Claude Code builtins.)
Replaced with the actions it stood for — push, open a PR, wait for CI,
merge, keep the worktree — plus the branch re-sync that was riding along
in the skill's tail:
```bash
git fetch origin && git reset --keep origin/main
```
That last part fills a real gap rather than just rewording the step. The
PR squash-merges, so the release branch stops being an ancestor of
`main`, and step 1's `git merge --ff-only origin/main` can't advance it
at the next release — step 1 already alludes to this ("it's only reset
to `main` after a release lands") without saying where the reset
happens. `--keep` moves the branch across and still fails rather than
discarding uncommitted work, per the data-safety rule on preferring the
failing variant. Doing it in step 11 also means a step 12 changelog
follow-up PR starts from the merged `main`.
Doc-only, and no test reads this file. `pre-commit run --files
.claude/skills/release/SKILL.md` passes; the Rust hooks skip since no
Rust changed.
> _This was written by Claude Code on behalf of @max-sixty_
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
1eded27943 |
Simplify forge, shell integration, and removal internals (#3662)
This consolidates cross-cutting models that had accumulated parallel representations, while preserving the CLI and config interfaces. ## What changed - Forge identity now flows through one `ForgeKind`, with boundary-safe network-host classification shared by CI, remote references, and structured repository metadata. Branded SSH aliases such as `github-personal` remain outside provider dispatch and receive an actionable `forge.platform` diagnostic when forge data is requested. - Worktree removal now uses one owned target type and rechecks uncached worktree topology immediately before compare-and-swap branch deletion, retaining branches that gained a live or locked checkout. - Shell integration no longer implements the retired single-file directive writer. Stale wrappers receive repair guidance, execution fails closed, and child processes cannot inherit the retired sourceable-file capability. - Zsh and Git-version probes are shared, while dead mocks, redundant dependency edges, serializer detours, pass-through types, and duplicate tests are removed. The removal guard deliberately distinguishes live, stale-prunable, and locked registrations. The detached cleanup path performs the same record-aware check before deleting a branch ref. ## Testing `cargo run -- hook pre-merge --yes` passed after merging current `origin/main`: 4,489 tests, Clippy, formatting, lockfile checks, docs, doctests, and snapshot review. > _This was written by Claude Code on behalf of max_. |
||
|
|
7d5befa90e |
docs(release): tag the tip of main the drift check just verified (#3653)
Step 12 of the release skill told the runner to expect a single line from `git log <cut-from>..origin/main`, then to tag whatever a bare `gh pr view --json mergeCommit` resolved. Neither claim survives the step's own remedy for drift. **The check can't clear after its own remedy.** When a commit lands during the release PR's CI wait, step 12's fix is a follow-up squash PR adding changelog coverage. That adds both the drifted commit and the follow-up's own squash commit to the cut-from range, so the list is permanently longer than one line and "re-run the check" has no exit condition. In the 0.70.0 release the range held four lines at tag time. **The tag target was underdetermined.** `gh pr view` with no argument resolves a pull request by head branch, and the release worktree's branch outlives the release. Run in that worktree today it selects #3650, an unrelated fix that reused the branch after v0.70.0 shipped. At tag time it would have landed on the follow-up, but only because the follow-up happened to be opened from the same branch and merged last. The check now tests coverage instead of length: clean means the changelog at `origin/main` documents every user-facing commit in the range, however long the list has grown. The tag then targets `origin/main`, the same ref the check just read, so the two cannot name different commits. That is the right commit both when nothing drifted (the release squash commit) and when the changelog had to grow (the follow-up's squash commit), and it keeps the tag reachable from `main`, the guarantee the old parenthetical about the local branch HEAD carried. The target matters beyond bookkeeping because `release.yaml` builds the binaries and the GitHub release notes from the tree at the tag. The v0.70.0 release body carries the entries that exist only in the follow-up commit, since that is what was tagged by hand. > _This was written by Claude Code on behalf of max_ Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
3c887a14ce |
fix(picker): stop underlining links the picker can't carry (#3651)
The picker rendered its rows with hyperlinks enabled and then stripped the OSC 8 escapes afterwards, keeping the underline. A row showed an underlined `#3604` and `:11486` with nothing behind either. This is the follow-up to #3643, which established the "every link is underlined" policy; the picker was the one place the policy was being asserted about text that wasn't a link. The cause was one boolean answering two questions. `supports_hyperlinks(Stream::Stdout)` decided both whether the terminal draws OSC 8 (which is what lets a dev-server URL collapse to `:port`, since the URL rides inside the escape) and whether this render's output delivers the escape intact (which the picker answers differently, because skim mangles it). Each cell called the probe independently, so a row's cells could disagree, and the fix for one cell wouldn't reach the other. `LinkStyle` now answers both once per render: `Linked` (underlined OSC 8 link), `Unlinked` (the same short text, no escape and no underline) and `Expanded` (no OSC 8 in the terminal, so a dev-server URL prints in full and stays copyable). It lives on `LayoutConfig` and rides into `ColumnGrid`, so `--prs` rows rendered off-thread present references exactly as the worktree rows beside them do rather than hardcoding `false`. `Destination` pairs it with the width because one fact decides both: `wt list` writes to the terminal and gets the full width plus intact escapes, while the picker gets a narrower list (skim keeps the rest for the preview pane) and no escapes. The four post-hoc `strip_osc8_hyperlinks` calls in the picker are gone, since nothing emits links there to strip. Two things fell out. `render_cell` was carrying four `LayoutConfig` fields as separate arguments; it now takes the layout, which is also how it got back under clippy's 7-argument cap without a suppression. And the "collapse to `:port`" decision stayed tied to the terminal probe rather than to link emission, so the picker's URL column keeps its narrow form instead of expanding to the full URL. ## Reviewer notes `src/commands/list/layout.rs` holds `LinkStyle` and `Destination` with the rationale; `src/commands/list/collect/mod.rs` is where the picker and `wt list` diverge, keyed on the same `progressive_handler.is_some()` the surrounding picker forks already use. ## Testing `test_link_markup_is_uniform_across_a_row` renders one item under both styles and asserts the CI reference and the dev-server port agree: both underlined links under `Linked`, both plain with no escape and no underline under `Unlinked`. `test_format_cell` additionally pins that `Expanded` and `Unlinked` read the same for a PR reference, which has no URL to fall back on. Beyond the suite, checked against a scratch repo with a URL template: `wt list` emits the dim-plus-underline-plus-link form closing with `[24m` so the dim survives, and the picker's rows render `:12107` with no escape and no underline. The port still collapses in the picker, which was the regression risk worth confirming. > _This was written by Claude Code on behalf of max_ Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
79824f7122 |
feat(styling): underline every hyperlink (#3643)
The statusline underlined its PR reference but not the dev-server port, so nothing marked the port as clickable: ``` ~/w/worktrunk.test-suite-cpu ↕|💬 ↑17 ↓11 ^+585 -258 #3604 :11486 Fable 5 🌕 30% ``` Both links now route through a shared `worktrunk::styling::hyperlink(url, text)`, which emits the OSC 8 sequence and the underline together. It closes with `[24m` rather than a full reset, so a wrapping color (the CI verdict) or dim (a port nothing answers on) survives the link. The rule is recorded as policy in the `writing-user-outputs` skill. Link text is sized to fit a column (`#3604`, `:11486`) and reads as ordinary content, and color already carries state, so the underline is the only thing marking text as clickable. Text that is not a link stays plain: on a terminal without OSC 8 support, `wt list` still prints the dev-server URL in full, unadorned. Tests: a unit test pins the helper output, and a statusline test asserts both segments carry a helper-built link. Snapshots updated for the reordered escapes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) > _This was written by Claude Code on behalf of Maximilian Roos_ Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
062cf57d8e |
ci(affected): pin cargo-affected to the published 0.3.2 (#3634)
Both `Install cargo-affected` steps in `affected.yaml` installed from git with no rev, so the tool version on any run was whatever had last merged to cargo-affected's default branch. That cut both ways today: an upstream regression made `cargo affected run` abort with `git diff stdout was not valid UTF-8: invalid utf-8 sequence of 1 bytes from index 136455` on all three OSes, turning the advisory legs red repo-wide, and the fix (cargo-affected #69) then arrived the same way. Neither change is visible in this repo's history. cargo-affected publishes to crates.io as of 0.3.1, so it can carry a `version: "=X.Y.Z"` pin like every other `baptiste0928/cargo-install` block here. `=0.3.2` is what the unpinned installs already resolve to, so nothing changes behaviorally today. Two things it buys: - The crate joins the weekly tend pin sweep, which reads `version:` strings and now covers `affected.yaml`. The `cargo-install` action's own drift annotation starts firing for it too. - The existing "bump the `db-v{N}` cache marker if cargo-affected ships an on-disk schema change" rule becomes enforceable. It was unfollowable against a floating version, since the schema could change with no diff to notice. The pin appears twice, once per job, which is a new way to be wrong. A drift between the two hits `migrate_legacy_tables`, which drops and rebuilds the coverage tables rather than erroring, so selection silently degrades while the advisory job stays green. That is recorded in the workflow's cache-contract comment and in the tend skill's bump list. `cargo-affected`'s `rust-version` is 1.94, against this repo's pinned 1.96.0 toolchain. ## Testing The `affected tests (…, advisory)` legs on this PR exercise the change directly: they resolve `=0.3.2` from crates.io rather than from git. > _This was written by Claude Code on behalf of max_ Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
4202cffe61 |
fix(ci): split ci by cadence so coverage uploads on every main commit (#3608)
## What prompted this Getting #3605 green ran into codecov reporting a `base_commit` three commits older than the real merge-base. This audits whether our config causes that. ## The cause Codecov picks a PR's base by walking back to the newest ancestor that has a coverage report. It used the real merge-base for PRs #3480, #3532 and #3602, and a stale one for #3603 and #3605. The difference is whether the merge-base uploaded a report. **29 of the last 40 main commits did not.** `ci` had one concurrency group for main pushes, and GitHub cancels the *pending* run in a group whenever a newer one joins, even with `cancel-in-progress: false`. So the question is how long a run holds the group, and a run isn't done until its slowest job is: | job | duration on main | |-----|------------------| | `fast-checks` | 2 min | | `code-coverage` | 3-4 min | | `test (windows)` | 11 min | | `collect affected coverage (windows)` | 110-129 min | Each main run held the group for ~2 hours, so nearly every subsequent main push was cancelled while queued, taking the 4-minute coverage job with it. Every cancelled main run's `updated_at` lands within a second of the next push's `created_at`. The 2 hours is real work, not queue: 2-5s from `created_at` to `started_at`, then 108 minutes inside `cargo affected collect` — 4181 tests under `-C instrument-coverage` with a per-test LLVM profile, ~5 GB of profraw. ## The fix: one workflow per cadence The three groups of jobs have incompatible needs, and one group was serving all of them. | workflow | cadence on main | why | |----------|-----------------|-----| | `ci` | every commit, ~11 min | required gate + fast checks | | `coverage` | every commit, keyed per-sha | a skipped upload leaves later PRs on a stale base | | `affected` | sampled, ~2 h | a DB a few commits old still anchors a correct superset | `affected` keeps exactly the grouping it has today, so its sampling is unchanged and deliberate. It just no longer drags the other two along. ### Scope of the impact The posted `codecov/patch` check scopes to the PR's own GitHub diff, so a stale base did **not** score PRs against other people's lines. On #3605 the posted 91.66% is exactly `github.rs`'s 11/12, while the stale-base compare object reported 64/65 across 13 files. What a stale base costs: - `codecov/project` reports "compared to \<stale sha\>" - the patch `auto` target is the stale base's project coverage (0.02pp here) - the compare API object widens to `base..head`, which is what made the investigation look like silence Separately, `test`/`lint`/`fast-checks` also stopped completing on main. Nothing load-bearing rode on that (they already ran on the PR), but it left `tend-ci-fix` with nothing to watch, since it doesn't fire on cancelled runs. ## Two smaller fixes - `ignore: "**/tests/**"` compiles to `.*/tests/.*` (confirmed against codecov's validator), which needs a leading directory and so never matched `tests/` itself. Inert today since `cargo llvm-cov` reports only `src/` (verified against a downloaded `cobertura.xml`), but now correct if that changes. Now `tests/**`. - `fail_ci_if_error` gated on `github.repository_owner`, which is the *base* repo's owner on a fork PR too, so the soft-fail its comment describes never applied. It keys off the head repo now. ## Docs The API behaviour was ours to misuse, not codecov's to explain. Three traps, all confirmed against the live API: - `file_report/<path>/` 404s with `coverage info not found` because the route swallows the trailing slash into the path. Without it the endpoint returns `line_coverage`. - `?pullid=N` always compares the PR's **current** head. `?base=&head=` asks about an earlier commit. - the compare response has no `patch_totals` key, and `.name` is `{base, head}` rather than a string, so a filename lookup silently matches nothing. A working recipe already existed in `running-tend`, but that skill is scoped to CI. `tests/CLAUDE.md` owns coverage investigation, so the queries go there and `running-tend` points at them instead of keeping a second copy. Re-running the corrected query against #3605's failing commit reproduces the miss exactly: `src/git/remote_ref/github.rs:164`, the `gh repo set-default` hint, matching what the session eventually found by hand. ## This PR demonstrates it It changes no Rust at all, only YAML and markdown. Codecov still reported a **10-file, 111-line patch** on its first commit, because it based the comparison on `203603909` rather than the real merge-base `32f380a27`. Every main commit in between has no report: | commit | ci run | report | |--------|--------|--------| | `32f380a27` | queued | no | | `9645e3e13` | cancelled | no | | `bcd1ffdfd` | cancelled | no | | `8865f20ab` | cancelled | no | Every one of those 111 patch lines belongs to somebody else's merged commit. It passed at 100% only because those commits are well covered. > _This was written by Claude Code on behalf of @max-sixty_ --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> |
||
|
|
784de4aacb |
skills(running-tend): nightly defers repo-wide CI breakage to tend-ci-fix (#3320)
## What Adds a "Repo-Wide CI Breakage" note to the nightly reference (`.claude/skills/running-tend/references/nightly-cleaner.md`): a failure that reproduces identically on `main` and every PR belongs to `tend-ci-fix`, which fires on any `ci`-workflow `failure` on `main`, and a non-required job failing is enough to trigger it. It does not fire on runs that end `cancelled`, so the nightly records the breakage in its summary rather than assuming the handoff lands. Also updates the `SKILL.md` reference index, which listed only "branch naming" for a file covering three topics. ## Why A nightly run shipped its sweep PR, then while polling that PR's CI diagnosed an unrelated repo-wide `code-coverage` break and started a second fix it did not finish, stranding the work on a pushed branch with no PR. Two workflows chasing the same repo-wide break duplicate effort and can produce conflicting PRs. Which workflow owns that break is not derivable from the nightly's own context, and it is documented nowhere else in the repo's skills (the only other mention of ci-fix is a label description). That is the whole of what this PR adds. ## What was cut, and why Earlier revisions carried a session-budget rule as well: the session has no room for a second deliverable, so ship one thing and stop. That rested on a 3600s harness cap. The cap has been 10800s since tend 0.1.9 and becomes 21000s once the pending tend release lands, so the claim does not hold at current durations, and its evidence was a single run. Removed. The routing advice also needed a correction rather than a restatement. `tend-ci-fix` gates on `conclusion == 'failure'` (`.github/workflows/tend-ci-fix.yaml:18`), so a `ci` run cancelled by rapid pushes never triggers it. Presenting the handoff as reliable would send a real breakage nowhere, so the note names the gap. ## Status of the breakage that prompted this Resolved independently: `cache-apt-pkgs-action` no longer appears anywhere under `.github/` on `main`, so the pin mismatch the nightly diagnosed is gone. The orphan branch `fix/ci-cache-apt-pkgs-pin-sha` is still on the remote and is now moot. 🤖 Generated by the tend review-runs workflow ([run](https://github.com/max-sixty/worktrunk/actions/runs/28361631224)), revised since. > _This was written by Claude Code on behalf of max_ --------- Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Maximilian Roos <m@maxroos.com> |
||
|
|
17add1f6dd |
fix: drop companion log paths from the -vv end-of-run message (#3521)
The `-vv` end-of-run block printed `trace.jsonl` and `subprocess.log` as bare gutter lines under a headline that named only `diagnostic.md` — an unannounced list whose relationship to the headline lived in a code comment, and the third and fourth prints of the same directory in one run. The start-of-run pointer already names the log directory, and the report body already routes readers to the raw companions (`subprocess_log_path` in the template; the profile heading names `trace.jsonl` as its source), so the end block now announces only the one human-facing file: ``` ○ Diagnostics and performance profile saved @ ~/…/.git/wt/logs/diagnostic.md ↳ To report a bug, open an issue (https://github.com/max-sixty/worktrunk/issues/new) and attach a secret gist: gh gist create --web ~/…/diagnostic.md ``` The headline drops "Logs," (the raw logs were precisely what wasn't at that path), and the hint now ends with the copyable gist command, linking the plain `/issues/new` URL instead of the ~150-char prefilled-body URL. > _This was written by Claude Code on behalf of max_ Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
df5c5d68e1 | skills(running-tend): scope data-loss review hold to the diff, not the file (#3457) | ||
|
|
4e45cfaa68 | skills(running-tend): fix diagnostic-bundle path for linked worktrees (#3479) | ||
|
|
0c189e512c | docs(skills): fix stale FlagNote method reference in writing-user-outputs (#3451) | ||
|
|
e02453aa5f |
feat(list): demote branch-worktree mismatch from red flag to dim info (#3419)
Implements option 3 ("demote the presentation") from the design proposal
in #3415, for #3389: worktrees created by agent harnesses at their own
conventional paths (`.claude/worktrees/…`) made every `wt list` row
carry a red `⚑` at merge-conflict severity.
The list predicate, JSON fields, and Path-column behavior are unchanged;
only the presentation changes:
- `⚑` renders dim yellow (was red) in `wt list`, the picker, and the
`--help` legend: below the full-yellow actionable states (prunable,
locked), above plain dim.
- Priority within the status slot becomes `✘ > ⤴ > ⤵ > ⊟ > ⊞ > ⚑ > /`:
the yellow actionable states (prunable, locked) now outrank the
informational glyph instead of being masked by it. The JSON schema-1
`state` field follows the same priority; schema-2 booleans are
independent and unaffected.
- The inline notice on `wt switch` / `wt remove` / `wt merge` / `wt step
prune` is gone entirely: the `wt list` glyph is the state's one surface.
The `expected_path` plumbing is deleted end to end, so switching to an
existing worktree no longer expands the `worktree-path` template at all
(it only ran to feed the notice).
Before/after on a repo with agent worktrees:
```
+ claude/frosty-kilby-92c7d3 ⚑_ (red ⚑, reads as an error)
+ claude/frosty-kilby-92c7d3 ⚑_ (dim-yellow ⚑, reads as a note)
```
Option 1 from the design doc (narrowing the predicate so only genuine
collisions flag at all) can land separately on top of this.
Well-tested: the pre-merge gate passes (4390 tests), snapshots reviewed
line-by-line (glyph color and priority changes in list/help output; the
notice lines removed from switch/remove output), the removed-notice
tests renamed to pin the silence, and clippy is clean including
`--features shell-integration-tests`. Verified live: `wt list` shows the
dim-yellow flag; `wt switch`/`wt remove` print nothing about the
mismatch.
Thanks to @dmsmidt for the report in #3389.
> _This was written by Claude Code on behalf of max_
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
ea2a94a7a7 | docs(release): reconcile skill with CLAUDE.md maturity & lib-compat stance (#3426) | ||
|
|
550d6641b6 |
skills(running-tend): drop case-specific example from root-cause section (#3421)
Follow-up to #3414: drops the example paragraph citing PR #3390 / issue #3389 from the "Weigh the root-cause fix" section. Skills state the recurrence pattern generically; per-incident references require institutional memory the reader lacks and age into noise. The rule paragraph stands alone; the case remains documented in #3414 and the #3390 discussion. > _This was written by Claude Code on behalf of max_ Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
f2508771d2 |
skills(running-tend): weigh root-cause fix over lossy config workaround (#3414)
Adds a triage rule to `running-tend` capturing a maintainer-flagged bad case from the daily review-runs analysis. ## The bad case In [#3390](https://github.com/max-sixty/worktrunk/pull/3390) (triage of [#3389](https://github.com/max-sixty/worktrunk/issues/3389)), triage of the `⚑` `branch_worktree_mismatch` flag shipped a **docs-only** PR recommending users reconfigure `worktree-path = "{{ branch | basename }}"`. On [#3390 the maintainer pushed back](https://github.com/max-sixty/worktrunk/pull/3390#issuecomment-4928586504) — "why do we drop the prefix?" — then [explicitly asked to log it as a bad case](https://github.com/max-sixty/worktrunk/pull/3390#issuecomment-4928926873) and closed the PR. Two problems the rule targets: - **Lossy workaround presented as *the* fix.** `basename` collapses `alice/foo` and `bob/foo` to `foo`, colliding on one directory, whereas the default `{{ branch | sanitize }}` keeps them distinct (`alice-foo` / `bob-foo`). The collision downside only surfaced after the maintainer challenged it. - **Jumped to a docs workaround over the root-cause code fix.** The proportionate fix was teaching the path-match check to tolerate a dropped namespace prefix, not asking every affected user to adopt a lossy template. The "docs-only, no risk" framing masked a bad recommendation — low code risk ≠ good guidance. ## The rule A new `### Weigh the root-cause fix before shipping a config/docs workaround` subsection under Issue Triage, placed right after "Suggesting Aliases for Niche Feature Requests" (which encourages config-based deflection) as a tempering counterpoint: check whether a configurable workaround is lossy/foot-gunny, weigh a proportionate root-cause code fix first, and surface any recommended config's downsides up front. ## Gate assessment - **Gate 1 (confidence)**: Critical — directly flagged by @max-sixty ("log this as a bad case"). 1 occurrence is sufficient at Critical. - **Gate 2 (magnitude)**: Targeted one-subsection addition; evidence bar met by the Critical maintainer flag. Already recorded in the [review-runs-tracking evidence log](https://github.com/max-sixty/worktrunk/issues/3349#issuecomment-4923383823). Kept as a separate atomic PR from the open cargo-PATH skill note ([#3361](https://github.com/max-sixty/worktrunk/pull/3361)) — different concern, non-overlapping section. <sub>Run [29081037464](https://github.com/max-sixty/worktrunk/actions/runs/29081037464) · daily review-runs</sub> Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> |
||
|
|
18d2bef84b |
refactor(perf): canonicalize the benchmark / wt-perf system (#3403)
Implements the design proposal from this branch's first commit
(
|
||
|
|
c8de0d872a |
ci: move lychee link check to the nightly workflow (#3402)
Moves the lychee link check out of PR CI into the `nightly` workflow. Link health depends on external sites (429s, bot-blocking, link rot), which made `lint` the flakiest PR check, and breakage isn't correlated with the PR that trips it: the most recent main run's lint job went red on the docs.anthropic.com restructure (fixed in #3399) with no code change involved. The hook stays defined once in `.pre-commit-config.yaml`, moved to the `manual` stage, so the PR `lint` job, local `pre-commit run --all-files`, and the `wt merge` gate all skip it (the wt.toml Windows conditional existed only to skip lychee and collapses). The new `link-check` nightly job runs that stage with the pinned lychee and is wired into `create-issue-on-nightly-failure`, so breakage lands in the nightly-failure issue for tend instead of blocking unrelated PRs. Running through pre-commit rather than `git ls-files | xargs lychee` keeps file selection at one definition: pre-commit's `types: [file]` filter excludes the 16 tracked `.md`/`.txt` symlinks, which would otherwise false-positive on relative links resolved against the symlink's directory. The trade: a PR that introduces a genuinely wrong URL now merges green and is caught up to a day later, asynchronously. Verified locally that the default stage no longer selects the hook and that `pre-commit run lychee-system --all-files --hook-stage manual` reproduces the CI lint run's selection exactly (same two flagged inputs). The nightly job itself first runs on the next cron or a `workflow_dispatch`. > _This was written by Claude Code on behalf of max_ Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
9edf0aa6b3 |
feat(approvals): add wt config approvals list and clear --stale (#3380)
## What
Two additions to `wt config approvals`:
- **`list`** — shows every command the project config declares (hooks in
lifecycle order, aliases, commit-message guidance) grouped into
**APPROVED** and **UNAPPROVED** sections, using `wt hook show`'s symbol
vocabulary (`○` approved, `❯` requires approval). Approvals recorded for
commands no longer in the config (edited or removed since approval) are
listed in a separate warning block. Works without a project config
(recorded approvals still list, all stale). Output pages like `wt hook
show`.
- **`clear --stale`** — removes only those left-behind approvals,
echoing each removed command; valid approvals survive. Staleness is
recomputed under the approvals-file lock (`Approvals::revoke_stale`), so
an approval recorded concurrently by another process is never removed.
Conflicts with `--global` (staleness is defined by the current repo's
config). A missing project config is an error, matching `add` —
approvals are keyed repo-wide while the config resolves per-worktree, so
a branch that merely lacks the file must not classify everything as
stale and wipe the repo's approvals; intentional wipe-everything remains
plain `clear`.
The `list` stale-block hint points at `clear --stale` instead of the
all-clearing `clear`.
## Example
```
APPROVED
○ pre-merge:
┃ cargo test
UNAPPROVED
❯ post-start dev:
┃ npm run dev
▲ Approved commands no longer in project config:
┃ some removed command
↳ To clear stale approvals, run wt config approvals clear --stale
```
## Notes
- The approval-prompt rendering and `approvals add` share the new
`ApprovableCommand::{label,format_template}` helpers and
`collect_approvable_commands`, so the prompt, `add`, and `list` render
commands identically.
- `list` is strictly read-only over approval state; nothing here runs a
project command.
- Staleness matching normalizes deprecated template variables on both
sides, same as `is_command_approved`.
- Also documents the ✓-vs-○ rule (completed action vs state) in the
`writing-user-outputs` skill.
## Testing
Unit tests for `stale_approvals`/`revoke_stale` (normalization both
directions, no-op, project-entry removal); integration snapshots for
`list` (mixed, all-approved, stale-only, no-config) and `clear --stale`
(removal + follow-up list, nothing-stale, `--global` conflict, and
no-config erroring with the approvals file proven intact). Full
pre-merge gate green.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
> _This was written by Claude Code on behalf of Maximilian Roos_
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
5175153b39 |
Release v0.65.0 (#3351)
Release v0.65.0. Version bump plus the 0.65.0 changelog section. **User-facing changes in this release:** - **Picker `alt-x` flashes why a worktree wasn't removed** (#3336, #3350) — the keep/failure reason now appears in the picker header instead of only draining to stderr on exit. - **`-vv` diagnostics consolidate on `diagnostic.md`, led by the performance profile** (#3329). This entry's bullet was previously misfiled under the already-released `0.63.0` section (the PR merged after `0.64.0` was tagged); this release moves it into `0.65.0`, where the change actually ships, and drops a misleading "removed `profile.txt`" clause for a file that never existed. - **`wt remove` preserves your subdirectory position** (#3344, closes #3343, thanks @caillou for reporting). **Also included:** a fix to the release skill's reporter-finding grep — `sort -un` on `#`-prefixed lines collapsed the whole issue-reference list to a single entry (every line sorts as numeric 0), which nearly dropped @caillou's credit. Now sorts on the numeric field after `#`. The nightly cross-platform suite (full-tests linux/macos/windows, feature-powerset, release-target, nix-flake, minimal-versions) passed green on the cut-from tip before this PR was opened. > _This was written by Claude Code on behalf of max_ --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
baa12fe198 |
ci: watch nightly in Tend; issue on full-tests failure; release runs nightly (#3345)
Follow-up to #3332 (fast-checks split + nightly `full-tests`), landing the monitoring/wiring that makes `nightly` a reliable safety net for the cargo-affected rollout. The benchmarks-split half of the original follow-up was dropped — #3337 did it independently, and more completely. ## What changed - **Tend watches `nightly`** (`.config/tend.yaml` + the generated `tend-ci-fix.yaml`). `nightly` hosts `full-tests` (the full 3-OS suite that catches what cargo-affected under-selects), so a red nightly is actionable — Tend now opens a fix PR instead of leaving it silent. #3339's 0.1.9 regen left the watched list at `[ci, publish-docs]`; this adds `nightly`. Tend watches at the workflow level, so it also covers the other nightly jobs (minimal-versions, nix-flake, udeps). - **`create-issue-on-nightly-failure` now needs `full-tests`**, so a cron `full-tests` failure files a tracking issue (neither #3332 nor #3337 wired it in). - **The `release` skill dispatches `nightly`** (benchmark-free) and waits on it — a release's full linux/macos/windows validation once the PR path moves to cargo-affected selection and stops running the full `test` matrix. ## Notes - The generated `tend-ci-fix.yaml` line was hand-applied to match `.config/tend.yaml`; a `uvx tend@latest init` regen reproduces it. - Trade-off: watching `nightly` at the workflow level also signs Tend up for the noisier nightly jobs (resolution drift, nix sandbox). Accepted to keep the safety net green. ## Testing CI-config plus one skill; validated with `actionlint` and pre-commit. The Tend trigger and the release step only exercise on a real nightly failure / release run. > _This was written by Claude Code on behalf of max_ --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
820666741f |
ci: run benchmarks on their own daily schedule, split from nightly (#3337)
The criterion suite ran as a job inside `nightly.yaml`: ~80 min, checking performance rather than correctness, yet feeding nightly's failure aggregator and dominating its wall-clock. This moves it into a standalone `benchmarks.yaml` on its own daily schedule, decoupled from nightly's correctness checks. `benchmarks.yaml` runs on its own cron (3:47 UTC, offset from nightly's 5:37 so the two long runs don't contend) plus `workflow_dispatch`. The env block is carried over so the shared `test` cache key still matches (per `.github/CLAUDE.md`); the gist time-series append stays gated to `schedule` events; and a dedicated `create-issue-on-benchmark-failure` job keeps a broken bench harness visible, since benchmarks no longer ride nightly's aggregator. One behavior change: benchmarks no longer run on PRs at all. Previously the job ran on a PR carrying the `nightly` label or touching `Cargo.*`. `workflow_dispatch` against a branch covers on-demand bench runs. `nightly.yaml` loses the `benchmarks` job, its header line, and its `needs` entry in the failure aggregator. Docs updated: the `CLAUDE.md` benchmarks note (benchmarks no longer gate merges since they're off PRs) and the `running-tend` skill's `setup-nu` call-site list. This branch also merges current `main`, which includes #3324 (drop `cache-apt-pkgs-action` for plain `apt-get`); `benchmarks.yaml` installs zsh/fish the same way rather than reintroducing the dropped action. > _This was written by Claude Code on behalf of max_ Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
53a3a9d71f |
skills(running-tend): fix picker/PTY flakes, but never time out with zero output (#3271)
## Problem The ci-fix classification list in `.claude/skills/running-tend/SKILL.md` told the bot to **exit without a PR** on a flaky test: > **Flaky test** (known-flaky or first-seen PTY/shell test) — exit without a PR (same behavior as prior test-flake ci-fix runs). [Maintainer feedback](https://github.com/max-sixty/worktrunk/pull/3271#issuecomment-4823026124): *we should be trying to fix flaky tests.* That prohibition was wrong. ## Change Scoped to removing that rule. The bullet now says to **try to fix it**, with the right ci-fix output being a durable, maintainer-visible outcome (a focused fix PR or a tracking issue) rather than a session that times out with nothing. One-bullet docs change to the skill overlay; no code paths affected. --------- Co-authored-by: worktrunk-bot <worktrunk-bot@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Maximilian Roos <5635139+max-sixty@users.noreply.github.com> |
||
|
|
e9631c14d6 | docs(tend): fix stale alias docs anchor (/step → /extending) (#3249) | ||
|
|
69f666273d |
skills(running-tend): lead with wt aliases, link prior issues (#3248)
Refines the **Suggesting Aliases for Niche Feature Requests** guidance in the `running-tend` overlay, per maintainer feedback on #3244: > @worktrunk-bot for future: you should lead with the alias, not with a shell func, please adjust the `tend` guidance. I don't think there's even a need to mention a shell func; only have guidance about aliases > > Also note that there are past issues that we should have linked here Changes: - **Lead with a `wt` alias.** The prior guidance let the bot pick "shell function or `wt step` alias, whichever fits better", and the #3244 reply led with a shell function. The section now presents the `[aliases]` entry as the project's preferred extension point, with no shell-function route. - **Search for and link prior discussions before drafting.** The same create-or-switch ask has recurred (#518, #1726, #3005, #3244) without the earlier threads being surfaced; linking them lets the deflection read as a considered position. The skill states this generically — the specific issue numbers stay in this PR, not in the skill body, so the guidance doesn't age. - **Flag the remote-branch surprise in prose rather than baking it into the alias.** `wt switch <name>` auto-creates a local branch from `<remote>/<name>` ([switch.rs](https://github.com/max-sixty/worktrunk/blob/438d33ede6d6fcc08d0ff12a1949d9ab573f1856/src/commands/worktree/switch.rs#L1011-L1023)); a create-or-switch wrapper inherits that, which belongs in a caveat in the reply. - **Tightened the prose per the `writing-prose` skill**: collapsed the duplicated intro and callout, removed em-dashes and "X, not Y" framing, and stated each rule positively. Triggering thread: #3244 ([comment](https://github.com/max-sixty/worktrunk/issues/3244#issuecomment-4802985147)). > _This was written by Claude Code on behalf of max_ --------- Co-authored-by: worktrunk-bot <worktrunk-bot@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Maximilian Roos <m@maxroos.com> |
||
|
|
7e188821fc |
Polish profile summary line and surface profiler in bug-report docs (#3186)
Small follow-ups to the `wt config state logs profile` profiler (landed in #3184), polishing its output and making it discoverable when triaging perf reports. **Summary line consistency.** The profile summary mixed duration shapes — `32.00ms subprocess time` led with the value, but `traced 18.00ms` led with the label. `traced` was the only outlier, so this flips it to `18.00ms traced`. Now every measured magnitude (counts and durations) leads with its value; the derived metrics (`parallelism`, `peak`) keep their label since they aren't quantities with units. **Profiler in the bug-report path.** Every `-vv` run already writes `diagnostic.md`, and it now embeds a rendered performance profile — but nothing pointed reporters or triage at it. So: the FAQ's `diagnostic.md` entry now mentions the profile, the "profile a slow tab-completion" tip points at `wt config state logs profile` instead of "read `trace.log`", and the `running-tend` skill tells triage to read the Performance profile section first on slow-command reports. Snapshot diffs are exactly the `traced` reorder. `fmt`/`clippy`/lib tests/docs-sync all green locally. > _This was written by Claude Code on behalf of max_ --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
9a58ad27e8 |
refactor(output): route --dry-run previews to stdout (#3085)
Adopt one stdout/stderr rule across the CLI: stdout carries the command's answer in whatever format the user selected (human or `--format=json`), including `--dry-run` previews; stderr carries narration (progress, success/warning/error, hints, interactive prompts, `-v`/`-vv`). A `--dry-run` is the answer when nothing mutates, so the preview goes to stdout, matching `wt list` / `wt config show`, the wider ecosystem (`git clean -n`, `rsync -n`, `terraform plan`), and keeping paging working. Five commands had their `--dry-run` human preview moved from stderr to stdout: `wt hook <type>`, `wt step relocate`, `wt step prune`, `wt step copy-ignored`, and `wt config shell install/uninstall`. For split previews the human output mirrors exactly what `--format=json` already puts on stdout; narration the json omits (prune's skipped-young caveat, "nothing to do" no-ops) stays on stderr. The shell preview renderers return a `String` so the caller picks the sink: `--dry-run` to stdout, the interactive `?` re-preview to stderr. Documented in the `writing-user-outputs` skill and `src/commands/CLAUDE.md`. |
||
|
|
3ed9ec9251 |
docs(release): cover #3099 in v0.59.0 changelog; add post-merge drift check (#3101)
Two corrections stemming from one root cause: #3099 landed on `main` while v0.59.0 was being cut, after the changelog was written against an earlier `main` tip. - **CHANGELOG**: fold #3099 into the `wt step eval -v` entry (the `-v` expansion now renders as labeled `source`/`result` blocks across eval, switch, hooks, aliases, and `wt -v list`) and drop the stale `{{ template }} → result` phrasing. The v0.59.0 GitHub release notes are updated to match. - **Release skill**: `/gpk` squash-merges onto whatever `main` tip exists at merge time, so a commit landing during the PR's CI wait ships undocumented. Step 1 now records the cut-from tip, and step 12 verifies `git log <cut-from>..origin/main` is just the release commit before tagging. This checks only that nothing drifted in, rather than cross-referencing every PR against the changelog (which would flag intentionally-skipped dep-bump/CI commits). > _This was written by Claude Code on behalf of max_ --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
01242177d3 |
refactor(expand): label -v template expansion as source/result blocks (#3099)
Reworks the `-v` template-expansion view. It used to stack the template,
a lone `→`, and the result in one shared gutter; the arrow on its own
line read awkwardly and the shared gutter blurred input vs output.
Now each side is its own labeled block — an info header above a bash
gutter:
```
○ eval source
{{ branch | hash_port }}
○ eval result
12107
```
The two headers carry the input/output distinction, so both blocks keep
the same gutter with content at column 2 (no marker glyph, no in-gutter
indent). Multi-line templates and results follow the same shape, one
gutter line per source line.
`expand_template` emits no trailing blank. The standalone `wt step eval`
adds one after the view; in a command pipeline the executor already
separates the block from the next output (an unconditional trailing
blank there would trip the no-double-blank guard).
This view also surfaces in `wt switch -v`, hooks, aliases, and `wt -v
list`'s deprecation preview — all snapshots regenerated. Known rough
edge: `wt -v list` previews a deprecated `worktree-path` once per
worktree, and those previews now sit back-to-back without a separating
blank.
> _This was written by Claude Code on behalf of max_
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
c02f297f15 |
skills(running-tend): mark rate_limits as a parsed statusline field (#3034)
The "Agent App Integration Surfaces" weekly-check table listed the
statusline JSON fields `src/commands/statusline.rs` consumes, but it
predated rate-limit parsing:
`rate_limits.{five_hour,seven_day}.{used_percentage,resets_at}` is now
parsed (see `parse_rate_limits`, and #3029 added `-vv` logging for the
same windows), yet the skill still named "rate limits" as a *future*
candidate to watch for.
Updated so the parsed-fields list includes `rate_limits.*` and the
future-candidate example drops "rate limits" (keeping session cost / PR
review state). Without this, the weekly upstream-changelog scan would
keep re-flagging rate-limits as a newly-added field worth surfacing.
Doc-only change to a CI skill; no code or test impact.
Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
||
|
|
61abd8949e |
skills(running-tend): don't ship fixes you can't verify (#2921)
## What Adds a "Don't ship fixes you can't verify" subsection under `## Issue Triage` in `.claude/skills/running-tend/SKILL.md`. Codifies the rule that when the bug or proposed fix turns on runtime state the bot can't observe from CI (agent CLI hooks firing, shell integration, interactive prompts), the bot should comment and ask for the concrete symptom rather than open a fix PR premised on an unverified hypothesis. ## Why Triggered by maintainer feedback on #2917 ([comment](https://github.com/max-sixty/worktrunk/pull/2917#issuecomment-4550851223)): > respectfully this was quite low quality, and you should be much more skeptical of things you can't test yourself, like this (or maybe you could have, by running `claude` in a tmux, but it would have been hard) — my expectations would have been that you would at least have observed there was a lot of hidden state and you should have flagged that you weren't confident; please log as a bad outcome In #2917 the bot shipped a Claude Code hooks change premised on an LLM-written analysis pasted in the issue without flagging that the actual hook-firing behavior wasn't observable from CI. The maintainer's own observation on #2916 contradicted the premise (permission prompts don't set 💬 in the first place), so the proposed `PostToolUse` / `Stop` transitions were addressing a symptom that doesn't exist. The new rule should prevent this shape of false-confident fix: any PR whose correctness depends on agent-CLI hook semantics, interactive-shell behavior, or other state the bot can't drive end-to-end should be a comment-and-ask, not a PR. ## Scope One subsection, ten lines. No behavior change elsewhere. Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> |
||
|
|
0c219d44a3 |
docs(skills): fast-forward release branch to main before cutting a release (#2966)
The release worktree's branch only resets to `main` after a release lands, so between releases it can sit behind `main`. Cutting from a stale branch silently drops everything merged since — this came up live: a release was about to ship from a branch 8 commits behind `main`, which would have dropped a Windows console-storm fix, bare-repo UX fixes, and a `feat(list)` addition. Adds an explicit first step to the release skill that fetches and fast-forwards the release branch to `origin/main` with `--ff-only`, which **fails** (rather than creating a merge commit or discarding work) if the branch has diverged. Spelled out as raw git rather than `wt up` because the `up` alias rebases each branch onto its own upstream (`origin/release`), not `main` — so it would not bring main's commits onto the release branch. Subsequent steps are renumbered (12 → 13) and the two internal cross-references updated. > _This was written by Claude Code on behalf of max_ |
||
|
|
7063b3cca4 | skills(running-tend): install Nix during weekly MSRV bump (#2960) | ||
|
|
a871627bdf |
docs(skills): add data-loss-surface review to tend and release (#2945)
Adds a data-loss-surface review gate to the tend PR review notes and the release skill. Follow-up to #2939. Thanks to @jbeda for raising it. |
||
|
|
51e32f4425 |
fix(statusline): space the context gauge emoji from its percent (#2944)
The context gauge in the Claude Code statusline rendered the moon-phase emoji flush against the percent (`🌕42%`). Most terminals draw emoji as double-width and bleed the glyph into the cell to its right, so the moon visually collided with the digits. This adds a trailing space — `🌕 42%`. The flush-packing convention still holds for ASCII and narrow markers (`@+1`, `↓1`, `●`); emoji are the exception. That distinction is now documented in the `writing-user-outputs` skill so future output keeps it consistent. Tests, the three inline statusline snapshots, nine rate-limit `.snap` files, and the hand-authored docs example (auto-synced to the skill reference) are all updated. > _This was written by Claude Code on behalf of max_ Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
0ad836e33b |
docs(release): skip release-type prompt when the answer is obvious (#2914)
## Summary The release skill always asked the user to confirm patch/minor/major. When `cargo semver-checks` reports breaking changes (forcing minor pre-1.0), or when only bug fixes have landed, the answer is fully determined — the prompt is wasted ceremony. Replace the unconditional prompt with a judgment-based policy: - Proceed without asking when only one bump is plausible (semver-checks breaking → minor; fixes only → patch). Inform the user and continue. - Ask only when there's genuine ambiguity (new features without semver breakage; borderline cases). When asking, the choice is patch vs. minor — no novelty options like skipping a version number. Step 6 in the workflow list and the "Do not proceed until user confirms" absolute are updated to match. ## Test plan - [ ] Skill renders cleanly on next release run - [ ] No regressions in the surrounding workflow steps 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
2ff656d10d | skills(running-tend): open PRs for model-name drift, not issues (#2891) | ||
|
|
39cfd80b6b |
fix(statusline): reserve a fixed 5 columns instead of 20% of width (#2871)
The Claude Code statusline runs `wt list statusline` as a subprocess with all three standard streams piped, so `terminal_width()` can't detect the terminal directly and `detect_parent_tty_width()` walks the process tree to find a TTY. That fallback reserved 20% of the detected width for Claude Code's own UI messages, which scales badly: on a 200-column terminal it gave up 40 columns, far more than the chrome needs. This reserves a fixed 5 columns instead, so wide terminals keep nearly all their width and narrow ones still get a sensible margin (`saturating_sub` guards against terminals under 5 columns). Also adds a weekly tend maintenance task that scans the agent CLIs Worktrunk integrates with (Claude Code, Codex, Gemini CLI, OpenCode) for changes to the surfaces Worktrunk depends on — the statusline JSON schema, worktree-lifecycle hooks, and plugin install mechanisms — and files an issue when something relevant changes. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
ec62580c29 |
revert(hooks): keep docs on pre-start/post-start; code accepts both (#2857)
Per @max-sixty's [direction in #2838](https://github.com/max-sixty/worktrunk/issues/2838#issuecomment-4509447593): revert the docs portion of #2840 and keep the code. Docs continue to recommend `pre-start`/`post-start`; both names work in code so anyone who already followed the briefly-changed docs (e.g. @EcksDy) isn't stranded once a release ships these aliases. ## User-visible — back to `pre-start`/`post-start` - README, docs site, skill mirrors, `dev/*.example.toml`, `plugins/worktrunk/README.md`, `flake.nix`, `.config/wt.toml` - `src/cli/mod.rs` / `src/cli/config.rs` / `src/cli/step.rs` / `src/help.rs` after_long_help and example snippets — and the auto-synced `docs/content/` and `skills/worktrunk/reference/` mirrors - `wt hook --help` canonical subcommand names; completion advertises `-start` only - `HookType` Display via strum, serde `rename`, and clap `ValueEnum` name — all `pre-start`/`post-start`. The Rust variant identifiers stay `PreCreate`/`PostCreate` (internal; we already paid for that rename in #2840, and now the eventual flip is a Display-only change) - `HooksConfig` serde canonical fields ## `*-create` still works (kept code) - `wt hook pre-create` / `post-create` — CLI alias on the canonical subcommand - `pre-create` / `post-create` in config: top-level, `[hooks.*]`, and per-project, in string, `[table]`, and `[[array-of-tables]]` form. Mechanism: serde `alias = ...` on the field, plus a silent in-memory rename in `migrate_content()` so the round-trip in `unknown_tree` doesn't flag table forms as schema-unknown. - The pre-0.32.0 `post-create` fatal-load-error machinery stays removed — the name is reclaimed, and both forms load without error. ## Smaller bits - `valid_user_config_keys()` / `valid_project_config_keys()` append `pre-create` / `post-create` so the unknown-field round-trip skips them. `test_valid_*_keys_all_deserialize` skips both aliases (they can't sit alongside the canonical without a duplicate-field error). - `DEPRECATED_SECTION_KEYS` drops the `pre-start`/`post-start` entries #2840 added — `pre-start`/`post-start` are canonical again. - `find_pre_start_from_doc` / `find_post_start_from_doc` / `find_renamed_hook_key` / `is_non_empty_item` / `migrate_start_hooks_doc` and their tests are removed; the migration direction flips via a new `migrate_create_hooks_doc` (silent, mirrors the prior shape). - Test files `e2e_shell_post_create.rs` and `post_create_commands.rs` rename back to `_post_start_` (via `git mv`, so the rename shows as a rename). ## Testing `cargo run -- hook pre-merge --yes` — 3806 tests pass; the 10 failures are all `case_4` of `shell_wrapper::unix_tests::*` (nu-shell case; `nu` isn't installed in this runner; same failures occur on `main`). Also manually verified that a fresh `wt switch --create` against a project config with `[post-create]` loads cleanly with no unknown-field warning and the hook fires as `post-start`. ## Follow-up Per @max-sixty: in a couple of weeks, once a release with both-names-work is out and users have had a chance to upgrade, the docs flip is straightforward (most of it is in `src/cli/mod.rs`'s `after_long_help` and the doc-sync test propagates). Re #2838. Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |