## 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_
`wt list --format=json` schema 2 now has a published, machine-readable
contract at
[worktrunk.dev/schema/list-v2.json](https://worktrunk.dev/schema/list-v2.json).
The schema was already derived — `test_schema_generates` built one,
asserted it compiled, and threw it away, with a comment saying "until
the schema export ships." This ships it: `wt list --print-schema` prints
the document (a developer entry point alongside `--help-page`,
intercepted before clap), and a new step in `test_docs_are_in_sync`
commits it to `docs/static/schema/list-v2.json`, the same
generate-and-commit pattern as `llms.txt`. It shells out rather than
calling `schema_for!` because `JsonEnvelope` lives in the bin-only
`crate::commands` tree.
Two things had to be fixed for the document to be usable.
**The contract.** `schema_for!` generates under schemars' *deserialize*
contract, which marks a `skip_serializing_if` field required — nothing
supplies it on the way in. The first document I generated therefore
required `default_branch`, `upstream`, `pr`, `checks`, `summary` and
`vars` on every item, all of which the absence rule routinely omits, so
it rejected the output it documents. Generating under `for_serialize()`
fixes it.
**The vocabularies.** Four fields — `checks.status`, `display.state`,
`default_branch.integration.reason` and `worktree.operation` — were
`&'static str`, so the schema described them as bare strings. They are
now `JsonCheckStatus`, `JsonMainState`, `JsonIntegrationReason` and
`JsonOperation`, each converted from its domain enum by an exhaustive
match, so a new `CiStatus`, `MainState`, `IntegrationReason` or
`InProgressOperation` variant is a compile error rather than a value
silently missing from the published vocabulary. **The emitted JSON is
unchanged**; the existing envelope snapshot passes untouched.
<details>
<summary>Before and after, for one item</summary>
```json
// before — rejects its own output, and loses the vocabulary
"required": ["default_branch", "upstream", "pr", "checks", "summary", "vars", "display"],
"status": { "type": "string" }
// after
"required": ["branch", "head", "display"],
"status": { "enum": ["passed", "running", "failed"] }
```
</details>
## Testing
`test_schema_accepts_envelopes` validates a battery — every `CiStatus`
over both sources, every `MainState`, a populated worktree row, an
integrated row with an upstream and a dev server, plus the absent and
null arms of the absence rule — against the same document
`--print-schema` emits.
Validating proves nothing about a type the battery never instantiates,
so the test also pins every non-`Nullable_` type in the document to a
path that must carry a non-null value. A new `Json*` type fails until
the battery reaches it, and a row that stops populating one fails too —
the check reports the type names rather than leaving the gap to a
reader. This needed a `jsonschema` dev-dependency: schemars only
generates, and derives the document from the types without ever seeing
an envelope, so nothing otherwise tied the two together.
The test was confirmed to fail on the bug it exists for. Reverting to
`for_deserialize()` makes it report `pr`, `checks`, `summary`, `vars`
and `display.columns` as wrongly required.
The dependency is dev-only: `reqwest`, `rustls` and `async-trait` stay
unselected so no HTTP stack comes along, and `cargo tree --package
worktrunk --edges normal -i jsonschema` finds no path to it.
One direction it deliberately does not cover: a *loosening*. If a field
reverted to `&'static str` the schema would say `type: string` and
anything would validate. That direction is held by the compiler instead,
via the exhaustive matches.
## Notes for review
- `schema_document()` lives in `json_v2.rs` beside the types, not in
`help.rs`, so `--print-schema` and the test compile the same document
rather than two constructions that could drift on the contract setting.
- The lychee exclusion for `worktrunk.dev/schema/` follows the entry
directly above it: a generated link that 404s until the site deploys.
> _This was written by Claude Code on behalf of max-sixty_
`wt config create --project` writes a comment into the user's
`.config/wt.toml` — and `wt config create --help` prints the same text —
carrying a raw, unresolvable Zola link:
```
# When many repositories share one self-hosted host, name it once in user config with a [pattern-keyed `[projects]` entry](@/config.md#user-project-specific-settings) instead of repeating this block in each repo.
```
Every other cross-reference in that file is a plain URL (`… see \`wt
hook\` (https://worktrunk.dev/hook/) …`), because
`transform_config_source_to_toml` converts the `after_long_help`
markdown to plain text on the way into `dev/wt.example.toml`. This one
link isn't converted: `convert_markdown_links_for_config` matched link
text with `[^\]]+`, which stops at the first `]` — here the one closing
the nested `` `[projects]` `` span — so the regex failed to match and
the markdown survived verbatim. The line arrived with #3701; it's the
only link in either generated example file with a bracketed span in its
text.
## The fix
**One rule for `]` in link text.** `ZOLA_LINK_PATTERN`, earlier in the
same file, already solves this problem for the skill mirrors — it
alternates a backticked code span with any non-`]`-non-backtick char,
which is why `skills/worktrunk/reference/config.md` renders this very
sentence with a resolved URL while the TOML example didn't.
`convert_markdown_links_for_config` now uses that same class rather than
a second, weaker one. Brackets in these link texts always sit inside a
code span, so the class fits the shape exactly, and it covers `[[…]]`
array-of-tables names as well — these sections already document
`[[projects."…".post-start]]` pipelines, so a link naming one is the
next form to arrive. Regenerating produces the intended form:
```
# When many repositories share one self-hosted host, name it once in user config with a pattern-keyed `[projects]` entry (https://worktrunk.dev/config/#user-project-specific-settings) instead of repeating this block in each repo.
```
**A shape the regex declines now fails loudly.** Widening the class
fixes the shapes we know about; it can't fix the next one.
`finalize_skill_content` already handled that risk with a guardrail —
after the rewrite it scans for a stray `](@/…md` and panics with the
offending line, precisely because "the regex declined on an unexpected
character in the link text" is the expected failure mode.
`transform_config_source_to_toml` had no equivalent, which is why this
one reached `dev/wt.example.toml` and the `--help` output. That check is
now extracted into `assert_no_untransformed_zola_links` and called from
both surfaces, so the next unsupported shape is a test failure naming
the line rather than a raw `@/config.md` target in a user's config file.
## Why nothing caught it
`test_project_config_source_generates_example_toml` compares
`dev/wt.example.toml` against the output of this same transform, so an
unconverted link is "in sync" by construction — the sync test can't see
the difference between a link that converted and one the regex declined
to match. Two tests close that gap:
- `test_config_markdown_links_convert_to_plain_text` asserts the
transform's output directly. It fails on `main`'s regex with exactly the
reported symptom, and pins the forms already working (Zola page, Zola
page + anchor, absolute URL, two links on one line, the `[[…]]`
array-of-tables name) plus the case that must *not* convert — a bare ``
`[forge]` `` span is not a link and has to survive verbatim.
- `test_untransformed_zola_link_fails_the_config_transform` covers the
backstop itself: an unbalanced backtick in link text makes the rewrite
decline, and the assertion turns that into a panic naming the line.
## Files
- `tests/integration_tests/readme_sync.rs` — the shared link-text class,
the guardrail extraction and its second call site, and both tests.
- `dev/wt.example.toml` — regenerated by the sync test (one line).
- `tests/snapshots/…help_config_create.snap` — the same line, as `wt
config create --help` renders it.
Ran locally on the final state: `readme_sync::` (15), `test_help` (47),
`cargo clippy --tests --all-features`, and `cargo fmt --check`. All
green; the generated files are byte-identical under the new class, so
the sync tests pass without regenerating. The full gate runs in CI.
---------
Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Codex installs of the worktrunk plugin carried no skills. `codex plugin
add` copies the plugin into `$CODEX_HOME/plugins/cache/` via
`copy_dir_recursive` (codex-rs core-plugins), which handles only regular
files and directories — so the `skills -> ../../skills` symlink (and the
nested `reference/README.md` link inside the tree) were silently
dropped, and sessions load from that cache copy. Verified against
codex-cli 0.144.1 both by reading the tagged sources and by installing
this repo's plugin into a scratch `CODEX_HOME`: the installed root had
no `skills/` at all and `codex debug prompt-input` showed an empty
plugin skill inventory.
This PR cuts the plugin's `skills` symlink over to a **generated
real-file mirror** of the authored repo-root `skills/`, kept current by
a new `sync_plugin_skills_mirror` stage in `test_docs_are_in_sync`
(dereferences symlinks, deletes stale files, self-heals and fails on
drift — same pattern as the other generated mirrors). Repo-root
`skills/` stays the authored home: Gemini hard-probes it at the
extension root, the docs sync writes into it, and Windows checkouts read
it. Reversing the symlink direction instead would put a symlink at the
repo root, breaking Gemini's install copy and every Windows checkout —
which also surfaces a latent bug this fixes: symlinks materialize as
plain text files on Windows clones, so installs from a Windows checkout
shipped no skills to Claude or Codex either.
It also drops the Codex manifest's `skills: "./skills/"` key: with no
key, Codex scans `<plugin-root>/skills/` by convention
(`default_skill_roots` in `codex-rs/core-plugins/src/loader.rs`), and
the explicit path resolves to the same directory, so the key was
redundant — symmetric with the Claude manifest cutover in #3431.
**For the reviewer:**
- The 20 files under `plugins/worktrunk/skills/` are the generated
mirror (byte-identical to repo-root `skills/`; git stores shared blobs
once). `plugins/worktrunk/CLAUDE.md` → "Plugin skills are a generated
mirror" documents the rationale with codex-rs citations.
- `sync_plugin_skills_mirror` in
`tests/integration_tests/readme_sync.rs` is the sync stage (Step 3b of
the pipeline); it already proved itself once in this branch — merging
main regenerated `reference/{config,list}.md` in the mirror.
- `test_plugin_layout_is_consolidated` now pins the mirror shape
cross-platform (real directory, no symlinks anywhere under it),
replacing the unix-only symlink assertion.
**Verification:** fresh scratch-`CODEX_HOME` install now carries both
skills into the cache and `codex debug prompt-input` lists `worktrunk`
and `wt-switch-create`; `claude plugin validate` passes and `claude
--plugin-dir … plugin details` discovers Skills (2) plus all 8 hooks;
Gemini's repo-root path is untouched. A 2×2 probe matrix (manifest key
present/absent × real dir/symlink) confirmed key-absence changes nothing
and symlinks ship nothing.
> _This was written by Claude Code on behalf of max_
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Reviewing this real `wt list` output against the `writing-user-outputs`
guidelines surfaced several inconsistencies:
```
○ Showing 15 worktrees, 6 branches, 2 with changes, 8 ahead, 2 columns hidden. 1 task failed
▲ Some git operations failed:
plugins: working-tree-conflicts (fatal: Unable to create
'/Users/…/.git/worktrees/worktrunk.plugins/index.lock': File exists.)
↳ To create a diagnostic file, run with -vv
```
The footer and the warning used different vocabulary for the same
failure ("task" vs "git operations", the latter inaccurate since
CI/URL/summary tasks aren't git operations), the warning said "Some"
when the exact count was known, and the per-failure lines leaked
internal kebab-case `TaskKind` discriminants (`working-tree-conflicts`)
that appear nowhere in docs or help.
The same output now reads:
```
○ Showing 15 worktrees, 6 branches, 2 with changes, 8 ahead, 2 columns hidden; 1 task failed
▲ 1 task failed:
plugins: working-tree conflict check (fatal: Unable to create '…/index.lock': File exists.)
↳ To create a diagnostic file, run with -vv
```
## Changes
- **One term, matching counts.** Footer and warning both say "N task(s)
failed" ("task" is the documented term — the JSON docs already use it).
The mixed form becomes disjoint (`2 tasks failed, 3 timed out` instead
of `5 tasks failed (3 timed out)`), so the footer's "failed" count
always equals the number of lines in the warning below it.
- **Human task names.** `TaskKind::display_name()` maps each task to
user vocabulary (`working-tree conflict check`, `ahead/behind counts`,
`CI status`, …) in the failure warning, the stall footer ("waiting on
…"), and the drain-timeout diagnostic. The strum kebab-case form remains
for tracing and `-vv` diagnostics.
- **Footer clause join.** Semicolon instead of a period splice, per
house style; also fixes always-plural "1 worktrees"/"1 branches" in
`--branches` mode.
- **Buffered summary to stderr.** The `○ Showing …` line narrates the
table rather than being part of the answer (`--format=json` omits it),
so in buffered mode it moves to stderr and piped stdout ends cleanly
after the last row. The progressive path keeps it on stdout, where the
summary is a repainted row of the table region. This is most of the
198-file snapshot churn (the line moves from the stdout section to the
stderr section).
- **Docs generator reads both streams.** `parse_snapshot_raw` in
readme_sync previously embedded stderr *or* stdout; it now concatenates
both in terminal order. The generated docs are byte-identical before and
after the stream move, which is the check that the console blocks still
read like the terminal.
Two negative test assertions that guarded against surfacing these
failures (`working-tree-conflicts` in `tests/integration_tests/list.rs`)
were retargeted to the new display names so they don't pass vacuously.
## Testing
Pre-merge gate (all tests + lints) green; feature-gated clippy and the
PTY progressive-list tests (`shell-integration-tests`) run separately
and green, since the local gate skips them.
> _This was written by Claude Code on behalf of max_
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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>
Follow-up #1 from PR #2422 review: `sync_well_known_skills` and
`sync_llms_txt` panicked on every failure, while every other step in
`test_docs_are_in_sync` reports via `(Vec<String>, Vec<String>)`. They
now use the channel so a single sync run reports all problems together.
While in the file: dropped redundant `.exists()` prechecks before reads,
collapsed the `if exists() then read else String::new()` pattern for
`skill_file` to `unwrap_or_default()` (we write `expected` either way),
and shared `docs_content_page_names` from
`convert_console_blocks_in_docs` so the directory walk and
underscore-prefix filter live in one place.
Net diff is shorter than baseline (-3 lines). Leaf TOCTOU reads stay as
one-line panics — converting them to the channel was ceremony, not
improvement.
> _This was written by Claude Code on behalf of Maximilian_
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
Three follow-ups from #2419 review.
- **Bare close everywhere.** With inner snapshot wrappers gone from
command pages (#2419), no `AUTO-GENERATED` markers nest inside the
help-page region anywhere in `docs/content/*.md`. The mirrored close
(`<!-- END AUTO-GENERATED from \`wt cmd --help-page\` -->`) was the only
remaining variant of the close form; collapsed to bare `MARKER_CLOSE`.
- `src/help.rs::PageMode::emit_footer()` no longer takes `subcommand`.
- The help-page regex in `readme_sync.rs` matches via non-greedy `.*?`
to bare `MARKER_CLOSE`, with a comment pointing at the new invariant
test.
- `AUTO_GENERATED_MARKER_PATTERN` strip regex built from the constants.
Added **`test_no_nested_auto_generated_markers`** — walks
`docs/content/*.md` and fails if any `AUTO-GENERATED` open ever appears
inside an already-open region. This is the explicit invariant that
bare-close pairing depends on; if a future change tries to re-introduce
nesting (e.g., restore an inner snapshot wrapper around terminal
shortcodes), the test catches it before the subtle "regex chops region
at first inner close" failure mode lands.
- **Aligned error channel.** `convert_console_blocks_in_docs` now
returns `(Vec<String>, Vec<String>)` like its sibling sync steps, with
per-file error capture for `read_dir`, dir entries, and
`read_to_string`. The caller passes errors through the same `tag()`
aggregation as everything else, so a transient I/O failure on one file
no longer aborts the whole pipeline silently. Also fixes the matching
clippy warning (`is_some_and(|e| e == \"md\")` → `is_none_or(|e| e !=
\"md\")`).
- **Docs alignment.** `docs/CLAUDE.md` updated in two places where the
prose still documented the mirrored close as the canonical form.
Visual check via curl across 12 dev-server pages confirmed no leakage of
any prior marker form. Adversarial review (subagent /popper-style)
caught the stale prose; otherwise no regressions found.
Net diff: +113/-49 (the growth is the new invariant test and explicit
per-file error handling; structural simplification shows as -49).
## Test plan
- [x] `cargo test --test integration readme_sync` — 12 sync tests pass
(was 11; +1 for the new invariant test)
- [x] `cargo test --test integration` — full integration suite (1558
tests) pass
- [x] `cargo test --test integration
test_no_nested_auto_generated_markers` — new guard test passes; manually
verified it fires on synthetic nesting
- [x] Single-pass convergence verified: `git checkout docs/ && cargo
test --test integration test_docs_are_in_sync && git diff --stat`
produces an empty diff after the first run
- [x] Visual check via local Zola dev server: 12 docs pages
(\`/worktrunk/\`, \`/llm-commits/\`, \`/claude-code/\`,
\`/tips-patterns/\`, \`/merge/\`, \`/step/\`, \`/remove/\`, \`/hook/\`,
\`/list/\`, \`/switch/\`, \`/config/\`, \`/faq/\`) — only HTML comments
contain marker text, no rendered leakage
- [x] `cargo clippy --all-targets --all-features` — clean
- [x] `cargo fmt --check` — clean
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
Two follow-ups from #2418 review.
- **Test brittleness fix.** Three previously-separate sync tests had
data dependencies that nextest could interleave:
`test_readme_examples_are_in_sync` reads `docs/content/*.md`, which
`test_docs_quickstart_examples_are_in_sync` generates from snapshots.
Under parallelism, README sync could see stale docs and produce content
that required a second run to converge. Collapsed both into the existing
`test_command_pages_and_skill_files_are_in_sync` pipeline, renamed to
`test_docs_are_in_sync`. Steps run sequentially in dependency order;
single pass converges from a clean working tree.
Each step's errors and updated-file list are tagged with the pipeline
stage (`[command pages]`, `[standalone docs]`, `[README]`, etc.) so a
failure tells a developer which stage broke without reading the test
source. README failure also now reports `(N of M section(s) updated)`.
- **Dead inner snapshot wrappers.** `expand_command_placeholders`
wrapped each terminal shortcode in command pages with `<!-- ⚠️
AUTO-GENERATED from <snap> --> ... <!-- END AUTO-GENERATED -->` markers.
Command pages regenerate wholesale from `--help-page` each sync, so the
inner wrapper served no in-place-refresh purpose — it was dead weight
nested inside the outer help-page region's markers. Stripped from
`expand_command_placeholders`'s HTML branch; net -32 lines across six
command-page docs files.
The outer help-page region's mirrored close stays — it's load-bearing
whenever any nested `AUTO-GENERATED` marker appears in the body. Comment
in `src/help.rs:emit_footer` updated to explain the role. (My follow-up
note that the mirrored close was redundant turned out to be a
misanalysis — non-greedy `.*?` only safely pairs the open with the right
close when there are no nested closes; once the inner snapshot wrappers
were gone, the mirror became technically unnecessary, but keeping it
survives any future reintroduction of nesting at no cost.)
Renamed the test in `CLAUDE.md` (4 sites) and `docs/CLAUDE.md` (2 sites)
so the documented `cargo test` invocations resolve.
Net diff: −54 lines.
## Test plan
- [x] `cargo test --test integration readme_sync` — 11 sync tests pass
(was 13; minus the two collapsed)
- [x] `cargo test --test integration` — full integration suite (1557
tests) pass
- [x] **Single-pass convergence verified**: `git checkout docs/
README.md && cargo test --test integration test_docs_are_in_sync && git
diff --stat` produces an empty diff after the first run
- [x] Visual check via local Zola dev server: \`/worktrunk/\`,
\`/llm-commits/\`, \`/claude-code/\`, \`/tips-patterns/\`, \`/merge/\`,
\`/step/\`, \`/remove/\`, \`/hook/\`, \`/list/\` all render terminal
blocks cleanly with no leaked marker strings
- [x] Adversarial review of the consolidation (subagent /popper-style):
all 4 actionable findings (stale doc references, lost README count,
missing stage tags, etc.) addressed
- [x] `cargo clippy --all-targets --all-features` — clean
- [x] `cargo fmt --check` — clean
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
Follow-ups from #2417 review, plus adjacent simplifications.
- **Cross-crate share of marker constants.** `MARKER_OPEN_PREFIX` and
`MARKER_CLOSE` now live in `src/docs.rs`. `src/help.rs` (the
`--help-page` producer of help-region markers) and
`tests/integration_tests/readme_sync.rs` (the consumer + producer of
snapshot/section markers) both reference them — drift becomes a build
error rather than a silent mismatch.
- **Drop `MARKER_OPEN_HTML_PREFIX`.** The `-HTML` variant emitted
visually identical wrapping; the suffix only signalled "in-place
refreshable" but the `.snap` ID + terminal-shortcode body in
`DOCS_SNAPSHOT_MARKER_PATTERN` already discriminate that. Standardised
on a single open prefix and stripped the `-HTML` literals from
`README.md`, the four standalone docs files, and the test file.
- **Help-page marker uses the shared prefix.** The mirrored close (`<!--
END AUTO-GENERATED from \`wt <cmd> --help-page\` -->`) stays as-is
because adjacent regions need unambiguous pairing, but the open prefix
now goes through `MARKER_OPEN_PREFIX`.
- **Drop `MarkerType::output_format()` / `extract_inner()`.** Both had a
single trivial non-panic branch that existed only to enforce "no
Snapshot markers in README" via `unreachable!()`. Replaced with one
explicit assertion in `sync_readme_markers` — surfaces the invariant as
an actionable error message instead of a panic.
- **Collapse `format_replacement`.** `wrap_in_marker` is invoked once
for both output formats; only body construction varies.
- **`__WT_QUOT__` rename in `tests/integration_tests/user_hooks.rs`.**
Inlined the single quotes — the `.replace('__WT_QUOT__', \"'\")` was
unnecessary indirection (the outer raw-string literal already tolerates
`'`, and TOML / Tera don't care about embedded `'`). Removes a
name-conflation footgun with the unrelated `__WT_QUOT__` placeholder in
`src/docs.rs`.
Net diff: -7 lines.
## Test plan
- [x] `cargo test --test integration` — full integration suite (1559
tests) pass
- [x] `cargo test --test integration readme_sync` — all 13 sync tests
pass; `test_readme_examples_are_in_sync` now produces a stable README on
a clean run (verified by re-running multiple times — no further updates
after the initial regeneration)
- [x] `cargo test --test integration
test_args_indexing_and_length_in_hook_template` — confirms the
user_hooks single-quote inlining works through TOML and Tera
- [x] Visual check via local Zola dev server: \`/worktrunk/\`,
\`/llm-commits/\`, \`/claude-code/\`, \`/tips-patterns/\`, \`/merge/\`,
\`/step/\`, \`/remove/\`, \`/hook/\`, \`/list/\` all render terminal
blocks cleanly with no leaked marker strings (`AUTO-GENERATED-HTML`,
`__WT_OPEN2__`, trailing `|||`)
- [x] `cargo clippy --all-targets --all-features` — clean
- [x] `cargo fmt --check` — clean
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
Three small follow-ups + a placeholder rename, all from #2405 review
feedback.
- **Root-cause fix for `|||` corruption.**
`convert_dollar_console_to_terminal` was promoting blank lines inside
mixed `\$ cmd + output` blocks to extra `cmd=` entries, dropping the
blank from the rendered body and emitting a stray `\$` prompt (e.g.
`cmd=\"wt list|||\"`). Now distinguishes command-only blocks (blanks are
visual spacing in `cmd=`) from mixed blocks (blanks belong to body).
Removes the matching `trim_end_matches('|')` bandage in
`expand_command_placeholders`. New unit test in `src/docs.rs` covers the
mixed-block case.
- **Single-pass classification.** Folded the two filter chains over
`block_lines` into one loop with one match — fewer branches, no
duplicated predicates.
- **AUTO-GENERATED marker consolidation.** Within
`tests/integration_tests/readme_sync.rs`, the `<!-- ⚠️ AUTO-GENERATED
... -->` literal appeared in four producer / regex sites with subtly
different shapes (HTML vs plain, ID format). Factored into
`MARKER_OPEN_PREFIX` / `MARKER_OPEN_HTML_PREFIX` / `MARKER_CLOSE`
constants plus a `wrap_in_marker()` helper, and threaded those constants
into the regexes via `regex::escape`. Pure refactor — no docs/skill
files regenerated.
- **`__WT_OPEN2__` / `__WT_CLOSE2__` → `__WT_OPEN__` / `__WT_CLOSE__`.**
The `2` was meant to distinguish doubled-brace placeholders from
hypothetical single-brace ones, but Tera only treats `{{`/`}}` (not
single braces) as template delimiters — there's no second variant to
disambiguate from. Updated the producer (`src/docs.rs`), the test-side
decoder (`tests/integration_tests/readme_sync.rs`), the Zola template
(`docs/templates/shortcodes/terminal.html`), the docs-site `CLAUDE.md`,
and the regenerated `docs/content/{step,hook}.md`.
## Test plan
- [x] `cargo test --lib
docs::tests::test_convert_dollar_console_to_terminal` — exercises new
mixed-block case + existing command-only / multi-cmd / comment cases
- [x] `cargo test --test integration readme_sync` — 13 sync tests still
pass after the refactor and bandage removal
- [x] `cargo test --test integration` — full integration suite (1558
tests) pass
- [x] Visual check via local Zola dev server: `/merge/`, `/step/`,
`/remove/`, `/hook/`, `/llm-commits/`, and `/list/` all render terminal
blocks cleanly with no `|||` artifacts and no leaked placeholder strings
(`__WT_OPEN__` etc. don't appear in served HTML). The multi-command jq
recipes block on `/list/` continues to render comments as bash-styled
section headers.
- [x] `cargo clippy --all-targets --all-features` — clean
- [x] `cargo fmt --check` — clean
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
Adds static command-output blocks to the docs pages dominated by GIFs
(addresses #2403). The blocks are **driven from insta snapshots** so
they stay in lockstep with what `wt` actually prints — one source flows
to all three surfaces:
- terminal `wt <cmd> --help` (plain text, gutter-formatted)
- `docs/content/*.md` (colorized `{% terminal(cmd="...") %}` shortcode)
- `skills/worktrunk/reference/*.md` (plain `$ cmd\noutput\n` block)
## What changed
- **Six scripted snapshot tests** produce realistic output (`cargo
nextest run`, `flyctl scale count 0`, LLM-generated commit messages):
- `test_docs_merge_pre_merge_hook` — `wt merge` with pre-merge hook
- `test_docs_step_commit_llm` — `wt step commit` with LLM
- `test_docs_step_squash_llm` — three-commit squash with LLM
- `test_docs_merge_squash_llm` — `wt merge` (squash + LLM + merge) for
`llm-commits.md`
- `test_docs_remove_pre_remove_hook` — `wt remove` with pre-remove hook
- `test_docs_hook_pre_merge` — `wt hook pre-merge` direct invocation
- **Sync pipeline extension** in
`tests/integration_tests/readme_sync.rs`:
- New write-back pass `sync_cli_mod_example_bodies` fills the
```console``` body in each `<!-- wt <cmd> (docs-example) -->`
placeholder in `src/cli/mod.rs` from the registered snapshot. Runs
before `--help-page` reads the file.
- `COMMAND_PLACEHOLDER_PATTERN` extended to match three forms
(```bash```, `{{ terminal() }}` self-closing, `{% terminal %} body {%
end %}`). This **fixes a pre-existing bug** in `docs/content/list.md`
where the HTML-mode expansion was silently broken.
- Stripped trailing `|||` corruption that arises when blank lines in
snapshot bodies are interpreted as empty commands by
`convert_dollar_console_to_terminal`.
- **Docs page migration**:
- `src/cli/mod.rs` — replaced four hand-written ```console``` blocks
(merge, step, remove, hook) with `<!-- wt <cmd> (docs-example) -->`
markers.
- `docs/content/llm-commits.md` — replaced three hand-crafted HTML
blocks with `<!-- ⚠️ AUTO-GENERATED-HTML from X.snap -->` markers.
- **Refactor follow-up** in a separate commit:
- `BADGE_EXPERIMENTAL_HTML`, `SUBDOC_MARKER_PREFIX`,
`DEMO_MARKER_PREFIX` hoisted to `worktrunk::docs` so producer and
consumer stay in lockstep.
- `normalize_clap_help_fences()` consolidates the `text→` +
`console→bash` replacement pair shared by `--help-md` and
`help_reference_inner`.
- **CLAUDE.md note** documenting the `.gitattributes`
`linguist-generated=false` exemption requirement when adding skill-only
files (carryover from #2409).
## Test plan
- [x] `cargo test --test integration readme_sync` — all 13 tests pass,
idempotent
- [x] `cargo test --test integration test_help` — help snapshots updated
- [x] `cargo run -- {merge,remove,step,hook} --help` — clean gutter
formatting, no visible HTML comments
- [x] `cargo run -- hook pre-merge --yes` — full project gate green
(3355 tests)
- [ ] Visual check on dev site (maintainer — terminal shortcodes now
render with colors via ANSI→HTML; dark/light variants both expected to
look like the GIFs they replace)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Maximilian Roos <m@maxroos.com>
## Summary
Three small cleanups to the docs/skills sync pipeline (flagged by
`/simplify` review on #2404 but deferred at the time):
- **`readme_sync.rs`** — extract a `write_tracked(path, expected,
rel_path, updated)` helper that handles `fs::create_dir_all` +
`fs::write` + push-to-`updated_files`. Applied at five call sites:
`sync_command_pages`, `convert_console_blocks_in_docs`,
`sync_skill_files`, `sync_well_known_skills`, `sync_llms_txt`. Callers
keep control of the "is it different?" check so each site can apply its
own normalization (e.g., `trim_lines`) before comparing.
- **`sync_llms_txt`** — merge the two parse/group loops. Use `let-else`
to pull out `extra.group` when the frontmatter is first parsed, then
push directly into the `BTreeMap`. Drops both the intermediate
`Vec<(String, Frontmatter)>` and an unreachable `.expect("non-home pages
must declare [extra] group")` that only existed because the group was
looked up twice.
- **`.gitattributes`** — new file. Marks the 14 auto-generated outputs
as `linguist-generated=true` so GitHub collapses them in PR diffs.
Targets `skills/worktrunk/reference/*.md`, `docs/static/*.md` (the `.md`
symlinks), `docs/static/llms.txt`, and
`docs/static/.well-known/agent-skills/index.json`. Primary sources
(`docs/content/*.md`, `src/cli/mod.rs`) stay fully visible — `git
check-attr` confirms.
No behavior change; all 13 `readme_sync` tests pass, `cargo clippy
--all-targets --all-features -- -D warnings` clean, `cargo fmt` clean.
## Test plan
- [x] `cargo test --test integration readme_sync` — 13 passed
- [x] `cargo clippy --all-targets --all-features -- -D warnings` — clean
- [x] `cargo fmt --check` — clean
- [x] `git check-attr linguist-generated` on a sample of generated +
primary-source files — generated flip to `true`, primary sources stay
`unspecified`
> _This was written by Claude Code on behalf of @max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
## Summary
Implements the [llms.txt spec](https://llmstxt.org/) for the docs site:
- **`worktrunk.dev/llms.txt`** — curated index listing every doc page,
grouped by sidebar section (Commands, Reference) and ordered by weight.
Generated from `docs/content/*.md` front-matter.
- **`worktrunk.dev/<page>.md`** — clean-markdown versions of each doc
page, served as symlinks from `docs/static/*.md` into
`skills/worktrunk/reference/*.md`. Zola follows the symlinks at build
time, so there's no content duplication — just 13 symlink entries in
git, alongside the existing
`docs/static/.well-known/agent-skills/worktrunk` symlink.
## What changed
- 13 symlinks at `docs/static/*.md` →
`../../skills/worktrunk/reference/*.md`
- `docs/static/llms.txt` — generated output (checked in, same as
`skills/worktrunk/reference/` and `.well-known/agent-skills/index.json`)
- `sync_llms_txt()` + `extract_intro_prose()` +
`docs_content_page_names()` helpers in
`tests/integration_tests/readme_sync.rs`
- Step 4 added to `test_command_pages_and_skill_files_are_in_sync` — CI
catches drift if front-matter changes and `llms.txt` isn't regenerated
The new directory-walk helper (`docs_content_page_names`) is shared with
`sync_skill_files`, which had the same walk inlined.
## Test plan
- [x] `cargo test --test integration readme_sync` — 13/13 pass,
idempotent on re-run
- [x] `cargo fmt` + `cargo clippy --all-targets --all-features -- -D
warnings` — clean
- [x] `pre-commit run --files tests/integration_tests/readme_sync.rs` —
pass
- [x] `zola build` produces `public/merge.md` etc. as real 4814-byte
files (not symlinks) and `public/llms.txt`
- [ ] CI green
> _This was written by Claude Code on behalf of @max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
## Summary
- Converts the `post-create` → `pre-start` silent migration into a fatal
load error. Paves the way to later reclaim `post-create` as a
background-semantics counterpart to `post-start` without the silent flip
risk.
- Project config: rejects with `ConfigError`. User config: surfaces as
`LoadError::Validation` and best-effort load continues without the file.
- `wt config show` renders the error inline instead of hiding it.
Marked as **draft for consideration** — this is step 1 of the two-PR
sequence discussed in [#1571
(comment)](https://github.com/max-sixty/worktrunk/issues/1571#issuecomment-4291059416).
A follow-up PR (one release later) would perform the actual `pre-start`
→ `pre-create` / `post-start` → `post-create` rename. Supersedes the
closed [#2359](https://github.com/max-sixty/worktrunk/pull/2359).
## Test plan
- [x] `cargo test --lib --bins` (601 passing)
- [x] `cargo test --test integration` (1540 passing; 10 `case_4`
failures are local `nu`-not-installed environmental, unrelated to this
PR)
- [x] `cargo clippy --all-targets --all-features` clean
- [x] New integration tests:
`test_post_create_in_project_config_is_fatal` and
`test_post_create_in_user_config_warns_and_skips`
---------
Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
`ZOLA_LINK_PATTERN`'s link-text class `[^\]]+` rejected any `]`, so Zola
links whose text contained a code span with `]]` (e.g. `` `[[block]]`
``) never matched and shipped as dead `@/…md` references in skill files.
#2321 worked around two such links by rewording them — this fixes the
root cause and restores the original wording.
## The regex change
Alternate "a balanced `` `…` `` code span" with "any
non-`]`-non-backtick char" inside the link text:
```
\[((?:`[^`]*`|[^\]`])+)\]\(@/([^)#]+)\.md(#[^)]*)?\)
```
Forbidding bare backticks in the single-char branch is load-bearing:
without it, the regex can bridge across two unrelated code spans on the
same line. `docs/content/faq.md:63` is a live example — ``
[`worktrunk-sync`](https://github.com/…) `` followed much later by ``
[custom subcommands](@/extending.md#custom-subcommands) ``. With a
permissive single-char branch, the engine pairs backtick 2 with backtick
3 (spanning the `]` after `worktrunk-sync`) and matches the whole chunk
as one "link."
## Guardrail
Added an `UNTRANSFORMED_ZOLA_LINK_PATTERN` check in
`finalize_skill_content`. After the transform, any leftover `](@/…md)`
panics the sync test with the offending line — so a future regex miss
fails loudly instead of silently shipping a dead link.
## Doc restoration
Reverts the two rewordings from #2321 in `docs/content/extending.md`
(`[[block]]` now sits inside the link text again).
## Testing
`cargo test --test integration readme_sync` (12 tests) passes. Full
`cargo run -- hook pre-merge --yes` green locally (3291 tests, all
lints).
> _This was written by Claude Code on behalf of Maximilian Roos_
Co-authored-by: Claude <noreply@anthropic.com>
Follow-ups from #2316.
## What's in here
1. **Alias `-v` variable dump.** Added `format_alias_variables(ctx)`
alongside the existing `format_hook_variables(hook_type, ctx)`, with a
private `format_variables_table` helper sharing the alignment +
`(unset)` logic. Wired into `run_alias` before the announcement,
symmetric with the foreground hook path.
2. **Help-table drift test.**
`test_template_variables_table_matches_constants` parses the `##
Template variables` table out of `src/cli/mod.rs`, extracts `(kind,
var_name)` pairs, and asserts presence + group placement against
`ACTIVE_VARS` / `REPO_VARS` / `EXEC_BASE_VARS` / `ALIAS_ARGS_KEY` /
union of `vars_available_in(Hook(*))`. Uses public API only — no leak of
the private `hook_extras` helper. Adding a var to the constants without
updating the table (or vice versa) fails the test. Descriptions stay
free-form.
3. **Shorter `-v` help text.** `Verbose output (-v: info logs +
hook/alias template variable & output; ...)`.
## Example
```
\$ wt -v greet world
○ template variables:
branch = feature
worktree_path = _REPO_.feature
worktree_name = repo.feature
…
args = ["world"]
repo = repo
…
cwd = _REPO_.feature
◎ Running alias greet
○ Expanding greet
echo hello {{ args }}
→
echo hello world
hello world
```
> _This was written by Claude Code on behalf of @max-sixty_
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Converts ~29 string literals across 16 files from escaped form
(`"\\d+"`, `"{\"name\": 1}"`) to raw-string form (`r"\d+"`, `r#"{"name":
1}"#`) where possible. Strings containing control escapes (`\n`, `\t`,
`\u{1b}`, etc.) are left alone — raw strings can't represent those.
No behavior change; `cargo check/clippy -D warnings/fmt/test` all clean
locally.
> _This was written by Claude Code on behalf of max-sixty_
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
`docs/content/config.md` was rendering `$ wt list (markers)` as its
prompt line. The HTML branch of `expand_command_placeholders` used the
placeholder id (the snapshot-lookup key, e.g. `wt list (markers)`) for
the `cmd=` parameter of the generated terminal shortcode, so
disambiguation suffixes leaked into the rendered prompt. This swaps it
for `display_cmd` (the inner command from the source code block),
mirroring what `ExpandMode::Plain` already does and the fix that landed
for the plain pipeline in #2049.
After rebasing on top of #2057 (which unified the HTML/plain functions
behind `ExpandMode`), the fix is a one-line change:
`cmd=\"{placeholder_id}\"` → `cmd=\"{display_cmd}\"`, plus a comment
update explaining why. The `docs/content/config.md` line is the
auto-regenerated artifact.
## Test plan
- [x] `cargo test --test integration
test_command_pages_and_skill_files_are_in_sync` — passes;
`docs/content/config.md:856` reads `cmd="wt list"` with no `(markers)`
suffix
- [x] `cargo run -- hook pre-merge --yes` — all tests, lints, doctests
pass
- [x] `docs/content/list.md` unchanged
- [x] `skills/worktrunk/reference/config.md` unchanged (plain pipeline
was already correct via #2049)
> _This was written by Claude Code on behalf of Maximilian Roos_
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
The HTML and plain expand helpers in
`tests/integration_tests/readme_sync.rs` shared ~30 lines of
iterate/lookup/read/error-aggregation/replace boilerplate and differed
only in the regex, parse helper, and output format. Collapse them into a
single `expand_command_placeholders(content, snapshots_dir, mode)` that
dispatches on an `ExpandMode::{Html, Plain}` enum, and adopt the plain
regex (optional `$ ` prompt, two capture groups) as the unified pattern.
The `$ ` prompt alternative is a no-op on the HTML path because
`convert_dollar_console_to_terminal` (`src/docs.rs`, called from
`src/help.rs:391`) has already rewritten `$ `-prefixed console blocks
into `{{ terminal }}` inline shortcodes upstream. Plain mode skips that
step (`src/help.rs:388`), so both forms reach the regex on that path.
## Behavior preservation
- HTML path still uses the placeholder id (group 1) as the `cmd=`
parameter in the terminal shortcode — the existing `(markers)` display
bug in `docs/content/config.md:849` is untouched so the parallel fix in
`config-markers-cmd` remains minimal.
- Plain path still uses the captured display command (group 2) for the
`$ <cmd>` prompt, with the placeholder id driving snapshot lookup.
- No `.md` or `.snap` files change — verified via
`test_command_pages_and_skill_files_are_in_sync`.
Net: −32 lines (95 removed, 63 added) in
`tests/integration_tests/readme_sync.rs`.
## Test plan
- [x] `cargo test --test integration
test_command_pages_and_skill_files_are_in_sync` — passes with no file
drift
- [x] `cargo test --test integration readme_sync` — all 12 tests pass
- [x] `cargo run -- hook pre-merge --yes` — 2963 tests + doctests +
cargo doc + clippy + lints all green
- [x] `cargo fmt --all` applied, clippy clean
> _This was written by Claude Code on behalf of Maximilian Roos_
Co-authored-by: Claude <noreply@anthropic.com>
Skill reference files under \`skills/worktrunk/reference/\` had
unexpanded placeholders (\`<!-- wt list -->\` followed by a bare \`wt
list\` code block) where \`docs/content/*.md\` already inlined the full
snapshot output. Readers of the skill reference had to guess what \`wt
list\` actually looks like.
This adds an expansion path for \`--help-page --plain\` output that
mirrors the HTML pipeline through \`literal_to_escape\`, then strips
ANSI via \`ansi_str::AnsiStr::ansi_strip()\` instead of converting to
HTML. The placeholder regex accepts both \`$ wt <cmd>\` and bare \`wt
<cmd>\` forms — the two source-template conventions in
\`src/cli/mod.rs\` and \`src/cli/config.rs\` — and captures the inner
command separately from the placeholder id, so disambiguation suffixes
like \`(markers)\` drive snapshot lookup without leaking into the
rendered prompt.
Result: \`list.md\` gets the three \`wt list\` examples expanded, and
\`config.md\` gets the marker example expanded.
The regex and expansion function parallel the existing HTML path
(\`expand_command_placeholders\` / \`parse_snapshot_content_for_docs\`).
They aren't unified — the two functions are ~40 lines each and differ in
regex, parse helper, and output format, so unification via an enum is
tolerable cleanup for a follow-up.
> _This was written by Claude Code on behalf of Maximilian Roos_
Co-authored-by: Claude <noreply@anthropic.com>
The marker help text incorrectly stated markers appear at the "start" of
the Status column with examples like `🚧↑`. The rendering code
(`status_symbols.rs:300`) places markers at the end, after all git
symbols (`↑🚧`).
Replaced the hand-written example with the `<!-- wt list (markers) -->`
placeholder pattern, backed by a new `test_readme_example_list_marker`
snapshot test. In terminal `--help`, this shows `wt list` as a command
hint; in web docs, it expands to the actual colored table output.
> _This was written by Claude Code on behalf of Maximilian Roos_
Co-authored-by: Claude <noreply@anthropic.com>
When a deprecated section key (e.g., `[commit-generation]`) appeared in
the wrong config file (e.g., project config), the user got no useful
guidance — the key was silently filtered from unknown-key warnings, and
the deprecation system's rename advice was misleading since the renamed
key also doesn't belong in that file.
Changed `DEPRECATED_SECTION_KEYS` from `&[&str]` to
`&[DeprecatedSection]` with metadata about the canonical replacement.
`warn_unknown_fields` now uses `WorktrunkConfig::is_valid_key()` on the
canonical top-level key to determine whether the deprecated key belongs
in the current config type: if yes, skip (deprecation system handles
it); if no, warn "Key X belongs in Y config as Z".
> _This was written by Claude Code on behalf of @max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>
Replace hand-curated `required_sections` arrays in
`test_config_docs_include_all_sections` and
`test_project_config_docs_include_all_sections` with section lists
derived from `valid_user_config_keys()` / `valid_project_config_keys()`
(powered by the existing JsonSchema derive). Adding a new field to
`ProjectConfig` or `UserConfig` now automatically fails the test if docs
aren't updated — no need to also update the test.
Follows up on #1844 which added the project config test with a
hand-curated list.
> _This was written by Claude Code on behalf of @max-sixty_
Co-authored-by: Claude <noreply@anthropic.com>
Mirrors `test_config_docs_include_all_sections` (user config) for the
project config side. Verifies that the
`PROJECT_CONFIG_START`/`PROJECT_CONFIG_END` section in `src/cli/mod.rs`
includes all `ProjectConfig` sections (`[list]`, `[forge]`,
`[step.copy-ignored]`, `[aliases]`), excludes deprecated `[ci]`, and
documents at least one hook key.
Without this, a new field could be added to `ProjectConfig` without
appearing in the generated `wt.example.toml`.
> _This was written by Claude Code on behalf of @max-sixty_
Co-authored-by: Claude <noreply@anthropic.com>
The pointer to `wt hook --help` plus quick-reference of all three
formats is sufficient for the created `.config/wt.toml`. Users get
uncommentable examples for common hooks and a pointer to the full
reference. Removes the evaluation TODO added in #1835.
> _This was written by Claude Code on behalf of @max-sixty_
Co-authored-by: Claude <noreply@anthropic.com>
The project config example file (`dev/wt.example.toml`) was
hand-maintained with no sync tests, while the user config example had a
robust auto-generation pipeline. This brings both to parity — `mod.rs`
is the single source of truth, sync tests generate both example files,
and CI catches drift.
Hooks documentation (~70 lines of formats, template variables, and
per-type examples) replaced with a pointer to `wt hook --help` plus a
quick-reference showing all three hook formats (string, named table,
pipeline). The example file now focuses on project-specific settings:
`list.url`, `forge`, `step.copy-ignored`, and aliases.
The sync test infrastructure is refactored from a user-config-specific
function into shared `extract_config_section` and
`assert_config_example_in_sync` helpers that both tests call.
> _This was written by Claude Code on behalf of @max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>
The example config had all TOML values double-commented (`# #`), making
them look disabled-within-disabled. Config code blocks in the source
markdown now show actual default values uncommented, producing clean
single-commented lines in the generated `config.example.toml`.
Other fixes: `switch.no-cd` default was shown as `true` but code
defaults to `false`; `step.copy-ignored.exclude` was shown as
`[".cache/", ".turbo/"]` but defaults to `[]`.
> _This was written by Claude Code on behalf of @max-sixty_
Co-authored-by: Claude <noreply@anthropic.com>
Skill reference files are consumed by LLMs, not browsers. They contained
~400 HTML tags (`<b><span class=g>Commands:</span></b>`, `'`, etc.)
that are noise for LLM consumers.
The root cause: skill files were derived from web-formatted docs via
`transform_docs_for_skill()`, which stripped Zola shortcodes but passed
HTML through. The HTML came from two pipeline stages that only serve web
rendering: `post_process_for_html()` (CLI markers → HTML spans in prose)
and `convert_command_reference_to_html()` (ANSI → HTML in reference
blocks).
**For command pages** (7 files, ~350 occurrences): adds `--help-page
--plain` flag that skips web-specific transforms
(`post_process_for_html`, ANSI color codes, terminal shortcodes, demo
GIF expansion). The sync test routes command pages through this flag
instead of reading from HTML-formatted docs. The `--plain` output reuses
the same assembly logic (prose + reference block + subdoc expansion) but
produces clean markdown.
**For non-command pages** (5 files, ~50 occurrences): the HTML was in
hand-written terminal shortcode bodies (styled CLI output demos).
Changed the terminal body fallback in `transform_docs_for_skill` from
passing through raw HTML to calling the existing `strip_html()`
function. Also strips `rawcode` shortcodes. No new text processors added
— reuses existing infrastructure.
Key files: `src/help.rs` (parameterized with `plain: bool`),
`tests/integration_tests/readme_sync.rs` (new `generate_skill_from_help`
+ `finalize_skill_content` shared helper),
`src/commands/config/state.rs` (fix pre-existing `render_markdown_table`
→ `render_data_table` migration bug from ec6f6f24c).
> _This was written by Claude Code on behalf of maximilian_
---------
Co-authored-by: Claude <noreply@anthropic.com>
Commands containing `{{ }}` template expressions (e.g., `wt step eval
'{{ branch | hash_port }}'`) were the only blocks that didn't get full
Syntect highlighting on the docs site — they fell back to accent-only
color because Tera would interpret `{{ }}` in the `cmd` parameter as
template expressions.
This uses text placeholders (`__WT_OPEN2__`, `__WT_CLOSE2__`) that pass
through Tera safely. The terminal shortcode template replaces them back
to real braces before Syntect processes them. Also fixes double-encoding
of `"` in cmd parameters (the old `"` was getting double-encoded by
Syntect to `&quot;`), using a `__WT_QUOT__` placeholder for the same
reason — Tera has no backslash-escape mechanism for string literals.
The skill file generator (`transform_docs_for_skill`) was updated to
handle the new format: extracting `cmd` parameter values and `|||`
delimiters into `$ `-prefixed bash blocks, converting legacy `<span
class="cmd">` body tags, and fixing the `[^)]*` regex that broke on `)`
inside cmd values.
Net effect: all code blocks on the docs site now have consistent
multi-color Syntect highlighting, and skill reference files have clean
`$ command` blocks instead of raw HTML.
> _This was written by Claude Code on behalf of @max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>
The web docs content area fits ~101 monospace characters on desktop. The
previous 80-char wrap caused unnecessary line breaks in option
descriptions — e.g., enum variant descriptions wrapping mid-phrase like
"Stage everything: untracked files + unstaged tracked / changes".
Widens the two `help_reference()` call sites in `src/help.rs` from
`Some(80)` to `Some(100)`. Also fixes a pre-existing issue where clap's
line wrapping would break bold (`<b>`) spans across lines —
`ensure_line_resets` now tracks active SGR styles and re-opens them on
continuation lines.
> _This was written by Claude Code on behalf of @max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>
Shell code blocks on the docs site had inconsistent syntax highlighting.
Blocks with `$ ` prompt prefixes rendered as a single flat color because
Syntect's bash grammar treats `$` as variable expansion. This PR adds `$
` prompts to all shell commands while preserving full Syntect
highlighting by routing through terminal shortcodes.
## Approach
All shell commands in `console` blocks use `$ ` prefix.
`convert_dollar_console_to_terminal()` (new library function in
`src/docs.rs`) detects `$ ` lines and emits Zola terminal shortcodes:
- **Single or multi-command blocks** (no `{{ }}`): Uses `cmd` parameter
with `|||` delimiter. The shortcode template splits, highlights each
line individually through Syntect, and wraps commands in `<span
class="cmd">` (CSS `::before` adds `$ `). Comment lines (`#`) are
highlighted as comments without a prompt.
- **Blocks with `{{ }}` template syntax**: Falls back to body approach
with `<span class="cmd">` (accent color only, since Tera would interpret
`{{ }}` in the `cmd` parameter).
The function runs in both the `--help-page` generator (CLI source →
docs) and the doc sync test (hand-written docs → terminal shortcodes).
Hand-written docs can use plain `console` fences with `$ ` and get
auto-converted.
## Key files
- `src/docs.rs` — New library module with
`convert_dollar_console_to_terminal()` and unit tests
- `docs/templates/shortcodes/terminal.html` — Template enhanced to loop
over `|||`-delimited commands, highlighting each through Syntect.
Supports self-closing `{{ }}` syntax for bodyless blocks.
- `src/help.rs` — Uses library function, updated pipeline docs
- `tests/integration_tests/readme_sync.rs` — Sync test runs conversion
on all docs (not just CLI-generated). Updated skill transformation to
handle both body and self-closing terminal shortcodes.
- All `src/cli/*.rs` — `$ ` added to all console blocks
- All `docs/content/*.md` — Auto-converted to terminal shortcodes (zero
`bash` blocks remain)
> _This was written by Claude Code on behalf of @max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: worktrunk-bot <w@worktrunk.dev>
Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Follows up on #1766. Two changes:
1. **Unify hook type**: faq.md approval prompt used `post-create` while
hook.md used `pre-start`. Both now use `pre-start` for consistency.
2. **Drift detection**: Added `test_approval_prompt_styled_in_hook_page`
— runs `wt hook --help-page` and asserts the output contains the styled
terminal shortcode. If the cli/mod.rs approval example changes without
updating the `post_process_for_html()` replacement in help.rs, this test
fails with an actionable error message.
> _This was written by Claude Code on behalf of @max-sixty_
Co-authored-by: Claude <noreply@anthropic.com>
Zola trims leading whitespace from shortcode bodies, stripping the
two-space gutter that aligns `wt list` table headers with data rows. On
the web, columns drifted ~17px (2 characters) from where they should be.
`encode_leading_spaces()` in the snapshot-to-docs pipeline converts
leading spaces on the first line to ` ` HTML entities, which survive
Zola's trim and render as spaces in `<pre>` blocks.
Closes#1753
> _This was written by Claude Code on behalf of max-sixty_
Co-authored-by: Claude <noreply@anthropic.com>
The `bench-llm-commits` task in Taskfile.yaml has hand-maintained copies
of LLM tool commands that can drift from the single source of truth in
`dev/config.example.toml`. Adds a sync test that parses both files,
unescapes the different quoting styles (bash `'"'"'` vs TOML `\"`), and
compares the actual command strings. Tools in the Taskfile but not yet
in the config example (llm, aichat) are naturally skipped.
> _This was written by Claude Code on behalf of @max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>
The Claude/Codex recommended commands were duplicated across 4 locations
(Rust code, llm-commits.md, config.md, Taskfile) with no automated sync
— they were already drifting (missing `CLAUDECODE=` prefix, missing `-c
system_prompt=''`).
Now the double-commented entries in `config.example.toml` are the single
source of truth. `recommended_config()` parses them at runtime via
`include_str!` + `LazyLock`, and a new sync test verifies
`llm-commits.md` matches. Also fixes the existing drift in all copies.
> _This was written by Claude Code on behalf of @max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>
Move experimental badges from the start of description paragraphs to
after the heading text in web docs.
Uses empty `<span>` elements with CSS `::after` for badge text, so the
span doesn't affect Zola's heading slug generation or page TOC entries.
This avoids the need for `{#slug}` anchor overrides and keeps sidebar
TOC entries clean (no "experimental" suffix).
Before: `## wt step relocate` / `EXPERIMENTAL Move worktrees to expected
paths.`
After: `## wt step relocate EXPERIMENTAL` / `Move worktrees to expected
paths.`
> _This was written by Claude Code on behalf of @max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>
Replaces plain `[experimental]` text in web docs with styled pill badges
— small uppercase labels with a subtle border that read as metadata
rather than emphasis.
Canonicalizes all experimental markers to `[experimental]` (was a mix of
`[experimental]`, `(experimental)`, `(Experimental)`). One marker format
in cli.rs, one `.replace()` in the post-processing.
Also renames `colorize_ci_status_for_html` → `post_process_for_html` (it
handles badges and URLs too), adds a module docstring documenting the
full `--help-page` pipeline, and fixes a latent double-application of
the post-processor on subdoc content.
> _This was written by Claude Code on behalf of @max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
- Add per-page `<meta name="description">` to all doc pages — command
pages auto-generated from CLI `about`/`long_about` via new
`--help-description` flag, non-command pages manually written
- Add `<link rel="canonical">` URLs and JSON-LD structured data (WebSite
+ SoftwareApplication) on the homepage
- Add custom `sitemap.xml` template with `<lastmod>` dates and
descriptive homepage `<title>`
- Extract shared `extract_about_and_subtitle()` helper, eliminating
duplicated subtitle logic between `handle_help_description` and
`combine_command_docs`
- Fix broken anchor in faq.md (`#picker-summaries` →
`#branch-summaries-experimental`)
## Test plan
- [x] Full test suite passes (2713 tests via `wt hook pre-merge --yes`)
- [x] All lints clean (pre-commit, clippy, cargo fmt)
- [x] Doc sync test confirms auto-generated descriptions match CLI help
- [x] Zola build succeeds with all template changes
> _This was written by Claude Code on behalf of @max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
- Migrate `[select]` config → `[switch.picker]` with backward compat and
deprecation guidance
- Add `timeout-ms` to `[switch.picker]` (default: 200ms, `0` = no
timeout) replacing the hardcoded 500ms picker command timeout
- Add `test_config_docs_include_all_sections` to catch undocumented
config sections
## Test plan
- [x] 530 unit tests pass
- [x] 1058 integration tests pass
- [x] All lints pass (pre-commit)
- [x] Help text snapshots updated
- [x] Docs, example config, and skills auto-synced
- [x] Deprecation detection and TOML migration tested (including
snapshot)
> _This was written by Claude Code on behalf of @max-sixty_
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
## Summary
- Migrate from static CSS syntax highlighting to Zola's giallo engine
with custom `worktrunk-light.json` theme
- Replace hardcoded `syntax-light.css` / `syntax-dark.css` with
theme-based class generation
- Design a warm "sunlit workshop" palette: amber commands, gold strings,
chartreuse quoted strings, rusty constants
- Add CSS sibling selector to differentiate quoted from bare strings
(giallo tokenizes both as `z-string`)
## Test plan
- [ ] Verify syntax colors on `/switch/` (bash: commands, flags,
strings, quoted strings)
- [ ] Verify TOML blocks on `/config/` (section headers, keys, values)
- [ ] Verify dark mode is unaffected (quoted string CSS rule scoped to
`prefers-color-scheme: light`)
- [ ] Check all tests pass (`cargo test`)
> _This was written by Claude Code on behalf of @max-sixty_
---------
Co-authored-by: Claude <noreply@anthropic.com>