Commit Graph

291 Commits

Author SHA1 Message Date
Worktrunk Bot 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>
2026-08-17 01:48:23 -07:00
Maximilian Roos 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_
2026-08-15 09:22:15 -07:00
Worktrunk Bot 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 ec0f3a344 (#3771) made it structural:
`check_stderr_macros_come_from_styling` now requires every bare
`eprint!` / `eprintln!` under `src/` to resolve to anstream's.

The gap is exactly the mistake #3771 fixed in five files. A developer
following the skill's own example import — `use
worktrunk::styling::{eprintln, println, stderr};` — and then reaching
for `eprint!` mid-block gets std's macro, which keeps ANSI on a
redirected stderr where the `eprintln!` two lines up drops it. The skill
never names `eprint!` at all, and #3771's commit message notes the suite
cannot catch this: `CLICOLOR_FORCE=1` forces color on both printers, so
every snapshot agrees whichever macro is in scope.

One paragraph, placed directly after the `println!` one it mirrors: the
stripping asymmetry and its user-visible symptom, that `eprint!` is the
half that slips and why, the two ways to satisfy the rule (import, or
qualify as `styling::eprintln!(…)`), the guard test that enforces it,
and that `STD_STDERR_ALLOWED_PATHS` exempts whole files rather than
calls.

No regression test — the change is skill prose. The behavior it
describes is already pinned by `check_stderr_macros_come_from_styling`
and `test_stderr_narration_strips_ansi_when_piped`; what was missing is
that a developer reads the rule before tripping the guard rather than
after.

Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
2026-08-11 15:55:17 -07:00
Maximilian Roos 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>
2026-08-10 08:51:34 -07:00
Maximilian Roos 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>
2026-08-08 17:09:49 -07:00
Maximilian Roos 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>
2026-08-08 13:43:43 -07:00
Maximilian Roos 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>
2026-08-08 13:05:11 -07:00
Worktrunk Bot d146dc5957 skills(writing-user-outputs): route --format=json through print_json (#3750) 2026-08-08 05:47:39 -07:00
Worktrunk Bot 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>
2026-08-05 09:57:12 -07:00
Worktrunk Bot 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>
2026-08-05 05:28:38 +00:00
Worktrunk Bot 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>
2026-08-03 13:04:44 -07:00
Maximilian Roos 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>
2026-07-30 15:47:54 -07:00
Maximilian Roos 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_.
2026-07-30 02:10:07 -07:00
Maximilian Roos 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>
2026-07-29 00:19:16 -07:00
Maximilian Roos 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>
2026-07-28 21:08:13 -07:00
Maximilian Roos 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>
2026-07-28 20:04:15 -07:00
Maximilian Roos 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>
2026-07-28 13:11:00 -07:00
Maximilian Roos 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>
2026-07-26 20:19:15 -07:00
Worktrunk Bot 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>
2026-07-23 16:42:45 -07:00
Maximilian Roos 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>
2026-07-19 11:39:49 -07:00
Worktrunk Bot df5c5d68e1 skills(running-tend): scope data-loss review hold to the diff, not the file (#3457) 2026-07-15 02:22:43 -07:00
Worktrunk Bot 4e45cfaa68 skills(running-tend): fix diagnostic-bundle path for linked worktrees (#3479) 2026-07-15 02:18:16 -07:00
Worktrunk Bot 0c189e512c docs(skills): fix stale FlagNote method reference in writing-user-outputs (#3451) 2026-07-14 00:08:00 -07:00
Maximilian Roos 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>
2026-07-12 15:27:17 -07:00
Worktrunk Bot ea2a94a7a7 docs(release): reconcile skill with CLAUDE.md maturity & lib-compat stance (#3426) 2026-07-12 02:30:00 -07:00
Maximilian Roos 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>
2026-07-11 03:04:10 -07:00
Worktrunk Bot 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>
2026-07-10 16:26:20 -07:00
Maximilian Roos 18d2bef84b refactor(perf): canonicalize the benchmark / wt-perf system (#3403)
Implements the design proposal from this branch's first commit
(b22aced85, `design/simplify-bench-perf.md` — reviewable there; dropped
from the merge candidate per the `design/` convention). The perf
tooling's emission side was already canonical; this consolidates the
consumption side around one split: capture/harness work in `wt-perf`,
analysis in `worktrunk::trace` with `wt config state logs profile` as
its one CLI surface.

The seven design items:

1. **`wt-perf cache-check` deleted** — its output was the `cache` field
of `logs profile --format=json`. The tend statusline recipe now uses
`logs profile` (its old pipe also fed human-format stderr into a JSON
parser, so it's now correct as well as canonical).
2. **Timeline renderer moved into `worktrunk::trace`**
(`src/trace/timeline.rs`), sharing
`command_label`/`render_table`/`fmt_dur` with the profile so the two
views can't drift (they already had); drops wt-perf's `tabwriter` and
`insta` deps. Durations now render in the profile's fixed-point format.
3. **`Profile.by_context`** (BY CONTEXT table + JSON array):
per-worktree subprocess totals, the one analysis that previously
required trace_processor SQL. The SQL sections in `benches/CLAUDE.md`
and the `trace` module docs are replaced by a `logs profile` pointer;
Perfetto stays for visual critical-path work.
4. **One warm/cold bench runner** (`wt_perf::bench_wt`) owns the
`BatchSize::PerIteration` rationale once, replacing five inline copies.
All runs now assert child exit status (`benches/list.rs` previously
ignored it).
5. **Bench matrix pruned**: `real_repo` keeps only the 8-worktree
variants (scaling shape stays on synthetic `worktree_scaling`),
`cow_copy` deleted (benchmarked a hand-written serial copy against
production rayon — a settled choice), `remove_e2e/first_output` deleted
(duplicate of `first_output/remove`). Ends three gist series with this
PR as the single discontinuity point; saves roughly 15–20 min of the
~80-min daily bench run.
6. **wt-perf CLI tests moved in-package** (native
`CARGO_BIN_EXE_wt-perf`): the dummy `builds.rs` is gone and the nextest
setup script builds only `mock-stub`. Note `cargo test --test
integration` no longer runs these four tests; full-workspace runs still
do.
7. **`parse_config` returns a `SetupConfig` enum** covering `mixed-W-B`;
the two fixture builders share `init_bench_repo`.

**Merge with main (#3401):** the new prune benches and fixtures landed
mid-flight in exactly these files; the resolution routes them through
the consolidated API — `prune-M-U` is a `SetupConfig::Prune` variant,
the pair parsing is one shared `parse_pair`, and `prune-real` keeps its
own cache-managed path (no `--path`, self-repairing). `benches/prune.rs`
keeps its custom `iter_batched` arms deliberately: every variant asserts
the candidate count from stdout, which `bench_wt`'s shape doesn't
express — the exact carve-out the cache-handling docs describe.

Two review passes (correctness + subtraction) then folded in:
`CacheReport` trimmed to its same-context fields (the dropped six
duplicated `Profile` fields or counted cross-context noise the text
report never showed), `render_table` pads by display width (replacing an
ASCII-only invariant the new BY CONTEXT table violated), the timeline's
instant-event row gained test coverage, and Chrome args dropped the
redundant `duration_ms`.

**Decisions surfaced, not taken** (from the review passes): (a) `wt-perf
setup` could become `--persist`-only, dropping the interactive
Enter-to-cleanup path — a UX preference; (b) whether the daily benchmark
cron's gist time series justifies its remaining ~60 min/day is a
cost/value call. Also deferred: measuring whether `skeleton/cold` adds
signal over `skeleton/warm` before cutting it.

Gate green throughout (4374 tests); `clippy --features
shell-integration-tests` clean; `setup`/`timeline` (warm, cold,
chrome)/`logs profile` (text + JSON)/`trace` all driven end-to-end on a
scratch `mixed-4-8` repo.

> _This was written by Claude Code on behalf of max_

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 03:44:18 -07:00
Maximilian Roos 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>
2026-07-09 20:12:03 -07:00
Maximilian Roos 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>
2026-07-06 21:19:58 -07:00
Maximilian Roos 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>
2026-07-01 22:00:39 -07:00
Maximilian Roos 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>
2026-07-01 03:23:04 -07:00
Maximilian Roos 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>
2026-06-29 23:35:10 -07:00
Worktrunk Bot 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>
2026-06-28 05:57:58 +00:00
Worktrunk Bot e9631c14d6 docs(tend): fix stale alias docs anchor (/step → /extending) (#3249) 2026-06-26 06:35:53 -07:00
Worktrunk Bot 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>
2026-06-25 16:57:58 -07:00
Maximilian Roos 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>
2026-06-23 19:41:52 -07:00
Maximilian Roos 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`.
2026-06-17 10:49:15 -07:00
Maximilian Roos 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>
2026-06-16 21:42:47 -07:00
Maximilian Roos 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>
2026-06-16 18:52:59 -07:00
Worktrunk Bot 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>
2026-06-11 07:20:26 -07:00
Worktrunk Bot 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>
2026-06-09 09:46:20 -07:00
Maximilian Roos 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_
2026-06-02 13:09:20 -07:00
Worktrunk Bot 7063b3cca4 skills(running-tend): install Nix during weekly MSRV bump (#2960) 2026-06-02 08:03:21 +00:00
Maximilian Roos 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.
2026-05-30 12:28:06 -07:00
Maximilian Roos 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>
2026-05-30 10:40:18 -07:00
Maximilian Roos 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)
2026-05-26 17:00:06 -07:00
Worktrunk Bot 2ff656d10d skills(running-tend): open PRs for model-name drift, not issues (#2891) 2026-05-25 00:06:19 -07:00
Maximilian Roos 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>
2026-05-21 14:05:43 -07:00
Worktrunk Bot 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>
2026-05-21 16:03:03 +00:00