Skip to content

docs: rebuild the site with Astro and Starlight - #3866

Merged
max-sixty merged 6 commits into
mainfrom
codex/astro-starlight
Aug 22, 2026
Merged

max-sixty merged 6 commits into
mainfrom
codex/astro-starlight

Conversation

@max-sixty

Copy link
Copy Markdown
Owner

The docs now build with Astro and Starlight instead of Zola. This removes the Tera 2 migration blocker from #3827 while keeping the published routes, anchors, generated references, metadata, and crawler URLs stable. It replaces the approach closed in #3840.

What changed

  • Move the site into Starlight, with a custom Worktrunk homepage and a shared copper, gold, paper, and ink design system.
  • Keep generated docs as portable Markdown. The Rust sync pipeline no longer needs Zola shortcodes, template escaping, ANSI-to-HTML conversion, or reverse transforms for skill and README output.
  • Preserve terminal semantics and command-only copying, add responsive handling for short wide tables, retain stable heading IDs, and give repeated command-reference headings useful search labels.
  • Move docs CI and publishing to Node, Astro, and built-site contract tests. The tests cover public routes, links, assets, metadata, navigation, tables, terminal frames, and compatibility aliases.
  • Record demo themes from isolated environments, add a mobile core demo, and test the recording contract. The matching mobile assets are published in max-sixty/worktrunk-assets.

Closes #3827.

Verification

  • cargo run -- hook pre-merge --yes (4,660 tests)
  • npm --prefix docs run check
  • npm --prefix docs test (9 tests)
  • npm --prefix docs run build (16 pages)
  • npm --prefix docs run test:site (12 tests)
  • pytest docs/demos/tests/test_recording.py (4 tests)

This was written by Codex on behalf of max-sixty

@max-sixty
max-sixty force-pushed the codex/astro-starlight branch from 361d005 to 9e2e9cd Compare August 22, 2026 05:04

@worktrunk-bot worktrunk-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the merged tree at 282fbf8c. The shape is good — the Rust side is a net −620 lines, and dropping the ANSI→HTML round trip fixes a real defect: the skill reference files were shipping raw <span class=g> markup to agents (skills/worktrunk/reference/llm-commits.md was almost entirely HTML), and they're now clean text. I built the site locally and walked the route/asset/link contract; the .md companions and the .well-known/agent-skills/worktrunk symlink both dereference correctly into dist/, so those published URLs survive the move.

Two things I'd fix before merge, one inline each.

link-check is red on this head, and it's this PR's doing — 164 errors in run 32554191286, all of the shape file:///…/docs/public/config#wt-config-state-vars | File not found. The old config excluded "/@/" because Zola's internal-link syntax was validated by zola check rather than lychee; the replacement root-relative form (/config/#anchor) has no equivalent exclusion, so lychee resolves every one of them under root_dir and 404s. The coverage isn't actually lost — npm run test:site's "built pages have unique IDs and valid internal page links" checks every internal page link and fragment against the built site, which is stricter than what lychee could do on the sources.

Prose in captured output picks up state colors. semanticOutputSegments matches a bare + ([+↑⇡]\d*, zero digits allowed — asymmetric with the - branch's \d+), so cargo's own output renders a green bold +; [!?](?=\s|$) has no left boundary, so the approval prompt's ? renders amber. Both are visible in the current build:

$ grep -o 'unoptimized <span class="wt-positive">+</span>' dist/hook/index.html | head -1
unoptimized <span class="wt-positive">+</span>
$ grep -o 'remember<span class="wt-warning">?</span>' dist/faq/index.html | head -1
remember<span class="wt-warning">?</span>

Non-blocking:

  • sync_llms_txt now hardcodes the site title, description, and base URL that docs/config.toml's title / extra.site_description used to single-source for both the site and llms.txt. The description string is byte-identical to docs/src/content/docs/worktrunk.md's frontmatter description: and nothing enforces that — edit the homepage description and llms.txt keeps the old one silently. The loop already parses that page's frontmatter (it computes fm.description, then continues on slug == "worktrunk"), so lifting the value from there instead of a literal would close it. Related: Cargo.toml's affected rule now lists docs/astro.config.mjs under a comment saying the test "reads each of these at runtime (via fs::read_to_string)", but readme_sync.rs no longer reads it — harmless, just no longer true. And Head.astro carries a third copy of a site description (const description = 'Git worktree management for parallel AI agent workflows.'), distinct from the two above.
  • With PageMode::color() now returning ColorChoice::Never for both variants and transform_raw() an identity function, both are constant in self — the enum is still doing real work in process_body / emit_header / experimental_marker, but these two look like they want to be plain values rather than methods.
  • docs/scripts/parity.mjs defaults --reference to https://worktrunk.dev/, which is exactly what this PR replaces. Nothing references it (no CI job, and docs/CLAUDE.md's architecture table doesn't list scripts/), so after merge it compares the site against itself. Worth deleting with the migration unless it's meant to survive.
  • The built-site suite pins some counts that unrelated content edits will move — assert.equal(…configToc…length, 32) and assert.equal(outputOnlyBlocks, 1). Both carry good messages, so it's a judgment call, but adding a heading to config.md will fail a test about tables of contents.

I checked the deletion surface per this repo's review guidance: remove.md, faq.md, remove.rs, and the feat_custom_remove.json entry are all touched, but only as path strings and ANSI-stripped help rendering — no destructive command is added or widened, so no hold on that account. Holding the approval purely on the red link-check.

Comment thread .config/lychee.toml
Comment thread docs/src/plugins/worktrunk-terminal.mjs Outdated
Comment thread tests/integration_tests/readme_sync.rs Outdated

@worktrunk-bot worktrunk-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All three threads from the last review are closed. I re-verified each at this head rather than taking the commit messages for it:

  • Built the site and ran the pinned lychee (0.24.2, --offline) over the same tracked .md/.txt set the hook selects: no file:// errors from the PR's files, against 164 on 282fbf8c. (The two hits my run does show are on CLAUDE.md, which this review checkout reverts to the base version; the PR head's copy already carries the updated docs/src/content/docs/faq.md paths.) root_dir = docs/dist resolves directory routes — /config/#anchor → dist/config/index.html — and a planted /nonexistent-page/ still errors, so the links are genuinely checked rather than skipped. Better than the exclusion I'd suggested.
  • Rebuilt after the regex change: the wt-positive + census goes 181 → 178 and wt-warning ? 6 → 5, with both offenders gone (unoptimized + debuginfo on /hook/, Allow and remember? on /faq/) and every diffstat and status-column marker still colored. npm test 9/9.
  • readme_sync 16/16 including test_docs_are_in_sync; worktrunk.md's frontmatter title and description are byte-identical to the literals they replaced, so llms.txt is unchanged by the switch to single-sourcing. cargo clippy --all-targets --all-features -- -D warnings is clean after the PageMode trim, and mode still does real work in both call sites.

One non-blocking thing on the new link-check steps: they're a verbatim copy of publish-docs.yaml's four (same Node 24, same worktrunk-assets clone and cp target), and nothing keeps the two in step. If one side moves, link-check starts validating links against a site that isn't the one that ships, with nothing to catch the drift. The repo already has .github/actions/{test,tend}-setup, so a docs-build composite would make the two builds identical by construction.

@max-sixty
max-sixty merged commit 8e405bc into main Aug 22, 2026
45 checks passed
@max-sixty
max-sixty deleted the codex/astro-starlight branch August 22, 2026 07:02
max-sixty pushed a commit that referenced this pull request Aug 23, 2026
…#3867)

The Codex setup section in the LLM-commits docs describes the pinned
model as "the fast mini model", but the command it sits under pins
`gpt-5.6-luna` — which is not a `-mini` model. The prose was written in
#837 when the pin was a mini variant and was left behind when #3430
bumped `gpt-5.4-mini` → `gpt-5.6-luna`. This rewords the sentence to
describe what the command actually pins, following #3430's own framing
of `gpt-5.6-luna` as "the fast/low-cost variant of the current
recommended (5.6) family".

`docs/src/content/docs/llm-commits.md` is the primary (non-command doc,
per the sync taxonomy in `docs/CLAUDE.md`); the two mirrors under
`skills/` and `plugins/` were regenerated by `cargo test --test
integration test_docs_are_in_sync`, which now passes. The branch was
rebased onto `main` after #3866 relocated the docs tree, so the edit
lands at the post-#3866 path rather than the old `docs/content/` one.

No regression test: the change is a single prose sentence with no
behavior behind it. The sync test already pins the mirrors to the
primary, which is the only mechanical invariant here.

<details><summary>Nightly sweep context</summary>

Found during the rolling survey (bucket 23/28), reviewing
`skills/worktrunk/reference/llm-commits.md`.

Provenance:

```
$ git log --oneline -1 -S 'Uses the fast mini model' -- docs/content/llm-commits.md
cbe65a6 docs: add Codex config and optimize Claude Code for LLM commits (#837)
$ git log --oneline -1 -S 'gpt-5.6-luna' -- docs/content/llm-commits.md
c532d2d docs(llm-commits): bump Codex commit-generation model to gpt-5.6-luna (#3430)
```

A repo-wide `grep -i mini` turned up no other stale reference to the old
pin — the model string itself is consistent across `src/cli/mod.rs`,
`dev/config.example.toml`, `Taskfile.yaml`, and
`src/output/commit_generation.rs`; only this one sentence describes it.

</details>

Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
worktrunk-bot added a commit that referenced this pull request Sep 12, 2026
…ght ANSI values

The docstring said no value is a verbatim copy and to expect small deltas.
Neither holds: dark black/red/cyan are byte-identical to --wt-terminal-gutter,
--wt-terminal-red, and --wt-terminal-cyan, while the light ANSI hues sit 21-64
per-channel units from theirs because they still carry the light palette from
the pre-Starlight _variables.html (deleted in #3866).

Split the docstring's claim by group so it is true of every entry, and give
each light ANSI entry the site's current hex. The two parentheticals that said
'desaturated/muted from' lose that wording: #357a59 and #3d7f7f were derived
from the old --green #1b7f4b and --cyan #0a8080, not from today's values.

Whether to resync the light values themselves is left open - that would change
recorded GIFs.
worktrunk-bot added a commit that referenced this pull request Sep 12, 2026
The dark green, yellow, blue, and magenta values are byte-identical to the dark --green/--yellow/--blue/--magenta in the docs/templates/_variables.html #3866 deleted, and --wt-terminal-* did not exist until #3936 authored today's hexes. They are the same stale palette the light block carries, not values derived from the current properties, so they take the same annotation.

The docstring now states the rule the file follows: an ANSI hue quotes the site's current hex where the site moved and the theme did not.
max-sixty pushed a commit that referenced this pull request Sep 12, 2026
…and palette file (#4068)

Two references in `docs/demos/CLAUDE.md` point at files and directories
that do not exist, so anyone following them to change a demo's colors or
find a recorded GIF lands nowhere.

- The **Light/dark theme variants** section lists the outputs as
`docs/light/wt-core.gif` and friends. The build writes them to
`docs/public/assets/<target>/<theme>/<name>.gif` (`OUT_DIR` in
`docs/demos/build`), which is exactly what the directory-structure block
at the top of the same file already says — the two sections contradicted
each other.
- The same section points at `_variables.html` as the palette source.
That file was removed in #3866 when the site moved to Astro and
Starlight; the `--wt-*` custom properties now live in
`docs/src/styles/custom.css`, which is also what
`docs/demos/shared/themes.py` names in its own docstring.

Documentation-only, so there is no regression test to add; the claims
are verified against `docs/demos/build` and against the file's own
structure block.

Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
max-sixty added a commit that referenced this pull request Sep 12, 2026
The recorder's VHS theme still carried the Zola site's ANSI palette, which
#3866 deleted and #3936 replaced with --wt-terminal-*. Zellij, the starship
prompt, and Claude Code were configured light for both themes. Dark GIFs drew
Worktrunk's gutter as a near-white bar under a light Zellij tab bar, and the
light GIFs' yellow had 2.65:1 contrast against the page.

themes.py now reads the hex --wt-* properties from custom.css when the build
runs. The VHS theme maps ANSI colors the way the site renders snapshot output,
and DemoEnv carries the recording's theme so Zellij, starship, and Claude Code
follow it. This replaces the per-value "pre-Starlight" annotations.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQPDxs2csvGFGBr2j4MtrA
max-sixty added a commit that referenced this pull request Sep 12, 2026
The demo recorder kept its own copies of the docs site's colors, and
they had drifted from `docs/src/styles/custom.css`:

- The VHS terminal's ANSI colors were still the Zola site's palette,
which #3866 deleted and #3936 replaced with `--wt-terminal-*`. Light
yellow had 2.65:1 contrast against the page background; the site's
browser test requires 4.5:1 for its own dim terminal text.
- `brightWhite`, the background of Worktrunk's gutter, used the ink
color, so dark GIFs drew the gutter as a near-white bar.
- Zellij, the starship prompt, and Claude Code were configured for light
in both themes, so dark GIFs showed a light Zellij tab bar and Claude
Code's light theme.

`docs/demos/shared/themes.py` now reads the hex `--wt-*` properties from
`custom.css` when the build runs. The VHS theme maps ANSI colors the way
the site renders snapshot output (`terminal_color_class` and
`terminal_background_class` in
`tests/integration_tests/readme_sync.rs`). `DemoEnv` carries the
recording's theme, so Zellij, the starship palette, and Claude Code
follow it. This replaces the `(pre-Starlight; site is #…)` comments the
PR first added.

Recording from inside a Claude Code session also leaked into the Claude
demos. The demo's Claude Code inherited that session's `CLAUDE*`
variables and warned that transcript saving was off, and it started
Remote Control, which printed a live claude.ai session URL into the GIF.
The recorder now drops inherited `CLAUDE*` variables except
`CLAUDE_CODE_OAUTH_TOKEN`, and the demo's `settings.json` sets
`remoteControlAtStartup` to false.

The GIFs are re-recorded locally (`./docs/demos/build docs` and
`./docs/demos/build social`) and published in
max-sixty/worktrunk-assets@bb3bfba.

## Verification

- `pytest docs/demos/tests`: 4 passed.
- OCR of every 20th frame of the Claude demos (354 frames) finds no
`remote-control`, `claude.ai/code`, `rc active`, or `Transcript saving`.
The same check matches on frames from the leaking recording.
- Frames checked by eye in both themes for the ANSI colors, the gutter,
and the Zellij chrome.
- TUI validation passes for all nine Claude demo GIFs. The `wt-switch`
and `wt-statusline` checkpoints named absolute frames past the end of
these faster recordings (274–318 frames, against 344–465 in the
previously published GIFs), so a negative checkpoint frame now counts
back from the last frame; the omnibus checkpoint on its closing `wt
list` uses the same anchoring. The previously published GIFs pass the
new checkpoints too. Those checkpoints also forbid `claude.ai/code` and
`Transcript saving`, and a GIF made from a leaking frame fails on them.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01TQPDxs2csvGFGBr2j4MtrA

> _This was written by Claude Code on behalf of max-sixty_

---------

Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
Co-authored-by: Maximilian Roos <m@maxroos.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
social4hyq pushed a commit to social4hyq/homebrew-core that referenced this pull request Sep 20, 2026
worktrunk 0.75.0

Created-by: HarmonybrewBot
Commit-by: HarmonybrewBot
Merged-by: HarmonybrewBot
Description: Created by `brew bump`

---

Created with `brew bump-formula-pr`.<details>
  <summary>release notes</summary>
  <pre>## Release Notes

### Improved

- **The `wt switch` picker opens on one unified diff**: local rows combine committed, staged, unstaged, and untracked changes. Tab skips empty subsidiary views, while `Alt-1` through `Alt-8` retain direct access. [Docs](https://worktrunk.dev/switch/#interactive-picker) ([#3865](max-sixty/worktrunk#3865))

- **Git 2.43 is now the minimum supported version**: older Git exits with an upgrade message before Git-dependent commands run; `wt config shell` remains available so shell startup continues. (Breaking.) ([#3895](max-sixty/worktrunk#3895))

### Fixed

- **`wt list` no longer grows `.git/objects` during advisory conflict checks**: untracked content stays visible but is excluded from synthetic trees, and all probe-only objects use temporary storage. ([#3906](max-sixty/worktrunk#3906), fixes [#3883](max-sixty/worktrunk#3883), thanks @srobroek for reporting and the original fix)

- **`HEAD±` counts untracked files without inflating moves**: `wt list`, the picker, and statusline include untracked lines. Tracked deletions paired with untracked destinations count as renames, so pure moves are line-neutral and edited moves show only their edits. `HEAD±` now always detects renames, regardless of `diff.renames`. ([#3925](max-sixty/worktrunk#3925))

- **Timed child processes no longer abort `wt` in restricted sandboxes**: `wt switch --create`, picker pagers, and other bounded commands survive denied signal-handler wakes. TERM→KILL cleanup also returns promptly once the process group is gone. ([#3857](max-sixty/worktrunk#3857), [#3887](max-sixty/worktrunk#3887), fixes [#3856](max-sixty/worktrunk#3856), thanks @tomascamargo for reporting)

- **JSON list output ignores display-column gates**: `[list] columns` no longer makes `wt list --format json` contact a forge or generate summaries. CI requires `--full`; summaries also require `[list] summary = true` and a configured generator. (Breaking: schema 1 loses config-driven `ci` and `summary` fields.) ([#3812](max-sixty/worktrunk#3812), thanks @emeren for the request)

- **Long Windows paths compare consistently**: paths beyond 260 characters could retain a `\\?\` prefix and appear to be on another drive. `copy-ignored` then refused them, while switch, remove, merge, and relocate landed at the worktree root instead of the original subdirectory. ([#3899](max-sixty/worktrunk#3899), fixes [#3898](max-sixty/worktrunk#3898), thanks @Persedes for reporting and verifying the fix)

- **Shell configuration rechecks before it writes**: overlapping installs lock and reread rc files; Fish completion installs preserve files created after preview; uninstall applies only previewed rc removals and rejects changed Worktrunk-owned files. ([#3853](max-sixty/worktrunk#3853), [#3924](max-sixty/worktrunk#3924))

- **`-vv` profiles exclude their own collector commands**: command counts and cache summaries no longer include duplicate-looking work performed only to assemble the diagnostic report; raw traces still retain it. ([#3900](max-sixty/worktrunk#3900))

- **Fenced HTML comments survive picker Markdown rendering**: PR descriptions and comments now preserve fenced `<!-- … -->` lines; fenced `<!-- wt list … -->` markers also no longer affect the following block. ([#3908](max-sixty/worktrunk#3908))

### Documentation

- **Agent CLIs without a plugin can publish activity markers**: the integration guide now specifies the session-start, turn-end, and session-end calls, working-directory requirement, error guard, and cleanup contract. [Docs](https://worktrunk.dev/claude-code/#agent-clis-without-a-plugin) ([#3848](max-sixty/worktrunk#3848), thanks @AsafMah for requesting generic-agent guidance and @ortonomy for the related Pi use case)

- **Agent guidance explains worktree selection**: commands that name a branch already select its worktree; `-C` changes repository context and is needed only for commands without a worktree selector or callers outside the repository. ([#3890](max-sixty/worktrunk#3890))

- **The `wt up` recipe safely updates dirty worktrees**: it fetches all remotes, fast-forwards dirty branches without autostash, rebases clean branches, and continues past an ordinary refusal or a failed remote. [Docs](https://worktrunk.dev/extending/#recipe-rebase-every-worktree-onto-its-upstream) ([#3882](max-sixty/worktrunk#3882))

- **The docs site has a new responsive design**: rebuilt on Astro and Starlight while preserving public routes, anchors, and crawler URLs; generated reference pages remain synchronized. ([#3866](max-sixty/worktrunk#3866))

### Internal

- **Library API rework** (Breaking library API): `BranchDiffSpec` gained `working_base`, while remote-URL, shell-path, branch-push, approval, temporary-index, and repository helpers were removed. ([#3833](max-sixty/worktrunk#3833), [#3853](max-sixty/worktrunk#3853), [#3865](max-sixty/worktrunk#3865), [#3866](max-sixty/worktrunk#3866), [#3875](max-sixty/worktrunk#3875))

- **Codex loads repository maintainer skills**: `.agents/skills` now exposes the canonical `.claude/skills` tree; checkouts without symlink support keep the existing limitation. ([#3903](max-sixty/worktrunk#3903))

## Install worktrunk 0.75.0

### Install prebuilt binaries via shell script

```sh
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/max-sixty/worktrunk/releases/download/v0.75.0/worktrunk-installer.sh | sh && wt config shell install
```

### Install prebuilt binaries via powershell script

```sh
powershell -ExecutionPolicy Bypass -c "irm https://github.com/max-sixty/worktrunk/releases/download/v0.75.0/worktrunk-installer.ps1 | iex"; git-wt config shell install
```

### Install prebuilt binaries via Homebrew

```sh
brew install worktrunk && wt config shell install
```

## Download worktrunk 0.75.0

|  File  | Platform | Checksum |
|--------|----------|----------|
| [worktrunk-aarch64-apple-darwin.tar.xz](https://github.com/max-sixty/worktrunk/releases/download/v0.75.0/worktrunk-aarch64-apple-darwin.tar.xz) | Apple Silicon macOS | [checksum](https://github.com/max-sixty/worktrunk/releases/download/v0.75.0/worktrunk-aarch64-apple-darwin.tar.xz.sha256) |
| [worktrunk-x86_64-apple-darwin.tar.xz](https://github.com/max-sixty/worktrunk/releases/download/v0.75.0/worktrunk-x86_64-apple-darwin.tar.xz) | Intel macOS | [checksum](https://github.com/max-sixty/worktrunk/releases/download/v0.75.0/worktrunk-x86_64-apple-darwin.tar.xz.sha256) |
| [worktrunk-x86_64-pc-windows-msvc.zip](https://github.com/max-sixty/worktrunk/releases/download/v0.75.0/worktrunk-x86_64-pc-windows-msvc.zip) | x64 Windows | [checksum](https://github.com/max-sixty/worktrunk/releases/download/v0.75.0/worktrunk-x86_64-pc-windows-msvc.zip.sha256) |
| [worktrunk-aarch64-unknown-linux-musl.tar.xz](https://github.com/max-sixty/worktrunk/releases/download/v0.75.0/worktrunk-aarch64-unknown-linux-musl.tar.xz) | ARM64 MUSL Linux | [checksum](https://github.com/max-sixty/worktrunk/releases/download/v0.75.0/worktrunk-aarch64-unknown-linux-musl.tar.xz.sha256) |
| [worktrunk-x86_64-unknown-linux-musl.tar.xz](https://github.com/max-sixty/worktrunk/releases/download/v0.75.0/worktrunk-x86_64-unknown-linux-musl.tar.xz) | x64 MUSL Linux | [checksum](https://github.com/max-sixty/worktrunk/releases/download/v0.75.0/worktrunk-x86_64-unknown-linux-musl.tar.xz.sha256) |



### Install via Cargo

```sh
cargo install worktrunk && wt config shell install
```

### Install via Winget (Windows)

```sh
winget install max-sixty.worktrunk && git-wt config shell install
```

### Install via AUR (Arch Linux)

```sh
paru worktrunk-bin && wt config shell install
```
</pre>
  <p>View the full release notes at <a href="https://github.com/max-sixty/worktrunk/releases/tag/v0.75.0">https://github.com/max-sixty/worktrunk/releases/tag/v0.75.0</a>.</p>
</details>
<hr>

See merge request: Harmonybrew/homebrew-core!17938
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Zola 0.23 (Tera2) breaks the docs build — zola pin blocked at 0.22.1

2 participants