Skip to content

Warn when wt list can't parse the project config - #3993

Merged
max-sixty merged 2 commits into
mainfrom
land/project-config-load-error
Sep 3, 2026
Merged

max-sixty merged 2 commits into
mainfrom
land/project-config-load-error

Conversation

@max-sixty

Copy link
Copy Markdown
Owner

A .config/wt.toml that doesn't parse left no trace on wt list. The listing only reads project config through accessors that degrade to None ([list] url, [forge] platform), so a broken file could sit in the repo indefinitely while the command people run most often looked entirely normal.

ProjectConfig::load now returns the typed LoadError::File the user-config layer already uses, and wt list calls warn_if_project_config_unloadable() before collection, so the warning lands above the rows rather than between them:

$ wt list
▲ Project config @ _REPO_/.config/wt.toml failed to parse, skipping
  TOML parse error at line 1, column 11
    |
  1 | [pre-merge
    |           ^
  unclosed table, expected `]`

  Branch     Status   …
@ main       …

Commands that run project config — wt switch, wt merge, wt hook — still fail hard, because acting on a half-read hook list is worse than stopping.

Two smaller corrections ride along:

  • wt hook show and resolve_copy_ignored_config wrapped the load failure in .context("Failed to load project config"), which Repository::project_config() had already added. The message printed its own header twice, once as the error line and again inside the gutter. Both call sites drop the redundant wrap.
  • LoadError::File's display said Project config at <path>, while the warning that renders it says @. Both now say @.

One warning renderer serves both configs: emit_config_load_warning is factored out of emit_user_config_warnings, so the user and project lines can't drift.

UX survey items: #7.

Reviewable files: src/git/repository/config.rs, src/git/repository/mod.rs, src/config/project.rs, src/config/user/mod.rs, src/commands/list/mod.rs, src/commands/hook_commands.rs, src/commands/step/shared.rs. Generated mirrors and snapshots are regenerated.

This was written by Claude Code on behalf of max-sixty

🤖 Generated with Claude Code

https://claude.ai/code/session_01XAUYWFN9d9oh6jyoQiouHb

max-sixty and others added 2 commits September 1, 2026 16:23
`wt list` reached `.config/wt.toml` only through accessors that swallow
the error (`Repository::url_template`, `configured_forge_platform`), so a
file that no longer parsed left no trace on the command people run most,
while `wt switch` and `wt hook` failed hard on the same file. A broken
config could sit there indefinitely.

`ProjectConfig::load` now reports a parse failure as `LoadError::File` —
the typed error the user-config layer already collects — so both configs
render through one emitter, `emit_config_load_warning`, extracted from
`emit_user_config_warnings`. `Repository::warn_if_project_config_unloadable`
feeds it that error, and `handle_list` calls it ahead of `collect()`, so
the line lands above the rows in the shape the user config uses:

    ▲ Project config @ ~/repo/.config/wt.toml failed to parse, skipping
     ┃ TOML parse error at line 1, column 11

A failure with no file/parse split to render — an unreadable file, an
unresolvable path — warns with its own message; that message now names
the file it couldn't read, which it previously omitted. Commands that
*run* project config still fail hard: acting on a half-read hook list is
worse than stopping.

`wt hook show` and `wt step`'s copy-ignored resolution re-added
`.context("Failed to load project config")` on top of the identical
context `Repository::project_config` already attaches, so the failure
rendered as a header restating its own gutter line. Both removed. The
`Approvals::load` contexts alongside them are kept: their bodies name a
different operation and the file ("Failed to parse approvals file …"), so
the header frames rather than repeats. The path in that body now uses `@`
rather than "at", per the path vocabulary — which also covers the user
config's fatal-load message, since the two share one Display.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XAUYWFN9d9oh6jyoQiouHb
@max-sixty
max-sixty merged commit 10cb259 into main Sep 3, 2026
40 checks passed
@max-sixty
max-sixty deleted the land/project-config-load-error branch September 3, 2026 01:11
social4hyq pushed a commit to social4hyq/homebrew-core that referenced this pull request Sep 20, 2026
worktrunk 0.77.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

- **`wt list` sizes and aligns its columns to the terminal width**: one long branch name no longer sizes the Branch column for every row — it caps at 32 characters and elides, so a narrow terminal keeps its other columns instead of degenerating into a branch list. `Remote⇅` stops holding blank space open in a repo with no remote, and alignment now follows the value type. `--format=json` still carries the whole name. ([#3998](max-sixty/worktrunk#3998))

- **`wt list` names what it hid and marks detached worktrees**: the summary reads `hidden: Path, Commit` instead of counting columns, a detached worktree shows `⊘` rather than borrowing `⚑`, and a prunable row leaves its cells blank instead of showing loading dots that never resolve. ([#3998](max-sixty/worktrunk#3998))

- **`wt list --format=json` gains a `marker` field and stops reporting `detached` twice**: the branch marker set by `wt config state marker` is now readable without parsing it out of `symbols`, and `state` no longer reports a detached worktree as `branch_worktree_mismatch` — the sibling `detached` boolean carries it. ([#3998](max-sixty/worktrunk#3998))

- **`wt list --format=json` now defaults to schema 2**: callers get the envelope with repository metadata and orthogonal per-item facts without configuring `[list] json-schema`. `wt list statusline --format=json` follows the same key. Set `json-schema = 1` to retain the original bare-array format. (Breaking.) ([#4038](max-sixty/worktrunk#4038))

- **`wt config show` fails on a broken config**: it exits 1 on an unreadable or invalid config, an invalid `[list] columns`, or an invalid `approvals.toml`. Unknown keys and deprecations stay warnings and still exit 0. A new `APPROVALS` section counts project commands awaiting approval. (Breaking: it always exited 0 before.) ([#3999](max-sixty/worktrunk#3999))

- **Config migration output names its destination**: `wt config update --output <path>` writes the migration artifact to that file instead of applying it in place, and `--output=-` writes it to stdout. Output mode includes project config when run from a linked worktree. (Breaking: `--print` was removed.) ([#4021](max-sixty/worktrunk#4021))

- **Pi joins the agent integrations**: `wt config plugins pi install` writes an activity hook to `~/.omp/agent/hooks/pre/worktrunk.ts`, so Pi sessions show 🤖/💬 markers in `wt list` like Claude Code, Codex, OpenCode, and Gemini. `$PI_CONFIG_DIR`, `$PI_CODING_AGENT_DIR`, and named `$OMP_PROFILE`/`$PI_PROFILE` profiles are honored. [Docs](https://worktrunk.dev/claude-code/) ([#3594](max-sixty/worktrunk#3594), thanks @adity982, and @ortonomy for the request)

- **`wt config plugins codex install` installs the plugin too**: it previously registered the marketplace and left you to run `/plugins` in Codex yourself. Uninstall follows for both Codex and Claude Code, removing the plugin and then the marketplace, so the two commands are inverses. (Breaking: Codex uninstall previously left an installed plugin alone; Claude uninstall now also removes the marketplace.) ([#4019](max-sixty/worktrunk#4019))

- **`wt step copy-ignored` says whether it reflinked or copied**: the summary now reads `(reflinked, no extra disk)`, `(full copy)`, or a partial count, so the same `29.5 GB` line distinguishes a free copy from one that actually wrote the bytes. `--format=json` gains `reflinked` and `written`; a `--dry-run` plan no longer reports `files`/`bytes`. ([#4025](max-sixty/worktrunk#4025))

- **`wt config update` removes keys its destination cannot hold**: migrating `[select]` or `[commit-generation]` drops keys the new section has no field for, reporting each one. Otherwise the key landed at a path the user never typed and warned on every command, with no way to clear it. ([#3994](max-sixty/worktrunk#3994))

### Fixed

- **A failed `wt switch pr:<n>` against a fork no longer deletes a branch it didn't create**: the rollback force-deleted the branch on any setup error, including the one case where the branch wasn't its own — a `pre-switch` hook or a concurrent session claiming the name in the window after the forge answered. Branch and worktree are now created in one `git worktree add -b`, which writes nothing when the name is taken. ([#3984](max-sixty/worktrunk#3984))

- **Codex on Windows: the activity hooks no longer fail on every event**: each hook led with a bare `bash`, which `cmd.exe` resolves to the WSL launcher rather than Git Bash, so every event raised a `Hook failed` banner. Hooks now run `wt.sh` through a shim that finds Git Bash by path. ([#4008](max-sixty/worktrunk#4008), fixes [#4007](max-sixty/worktrunk#4007), thanks @McNultyyy for reporting and diagnosing)

- **Templates leave `branch` unset in a detached worktree**: aliases and hooks rendered `{{ branch }}` as the literal `HEAD`, which git resolves, so an unguarded template ran against the wrong ref — the reported case ended in `git push origin --delete HEAD`, which git refused. `base` and `target` follow. (Breaking: an unguarded `{{ branch }}` now errors.) ([#4010](max-sixty/worktrunk#4010), fixes [#4009](max-sixty/worktrunk#4009))

- **The OpenCode plugin loads under OpenCode 2**: OpenCode 2's loader dropped the old bare-function export silently, so activity markers stopped appearing. The plugin now exports one object carrying both the v2 `setup` and v1 `server` entry points, and spawns `wt` through `node:child_process` rather than the Bun shell. Re-run `wt config plugins opencode install` to pick it up; the one file covers OpenCode 2 and OpenCode 1.16 or later. ([#4018](max-sixty/worktrunk#4018), thanks @pragmaticivan for the request)

- **`wt step commit --branch` builds its prompt from the worktree being committed**: prompt-building git plumbing ran in the invoking worktree, so with nothing staged there the generated message described none of the changes it committed. `wt step relocate --commit` had the same bug, and `--dry-run`/`--show-prompt` now preview the worktree `--branch` names. ([#3996](max-sixty/worktrunk#3996))

- **LLM commit messages keep diffs for quoted paths**: a file whose name git quotes in the `diff --git` header — any non-ASCII name under the default `core.quotePath` — had its whole diff section dropped from the prompt once the diff exceeded the 100 KB budget. ([#4041](max-sixty/worktrunk#4041))

- **An unparsable user config is reported, not a panic**: `wt config show --format=json` and `--full`, plus `wt step prune`, `relocate`, `eval`, and `for-each`, panicked on a debug build and printed a bare `✗ Command failed` on a release one. They now report `✗ Failed to load config` with the parser's caret diagram. ([#4002](max-sixty/worktrunk#4002))

- **`wt switch --execute` names the directory the program actually starts in**: the `Executing (--execute) @ …` header rendered the worktree the background hooks run in, not the program's own directory — which differs under `--no-cd` or a switch from a subdirectory. The path is omitted when the shell already stands there. ([#4043](max-sixty/worktrunk#4043), fixes [#4042](max-sixty/worktrunk#4042), thanks @yajo for reporting)

- **`wt config plugins claude|codex uninstall` is safe to re-run**: removing an already-absent marketplace reported a failure, and a Claude uninstall that removed the plugin and then failed left the marketplace with no way to clear it. Both now finish a half-done uninstall and succeed when there is nothing left to remove. ([#4033](max-sixty/worktrunk#4033), [#4034](max-sixty/worktrunk#4034))

- **`wt config state marker` set and clear no-op outside a git repository**: they exited 1 with a git error, so an agent plugin running outside a repository printed one every turn (the hook's `|| true` kept the session going). ([#3981](max-sixty/worktrunk#3981), thanks @mahirhir)

- **`wt list` produces output in a bare repo with no worktrees**: it printed nothing at all and exited 0. It now reports `○ No worktrees` with a hint, `--branches` lists the branches, and `--format json` emits the envelope with an empty `items`. ([#3992](max-sixty/worktrunk#3992))

- **`wt list` warns when it can't parse the project config** rather than ignoring it silently; the listing still succeeds. ([#3993](max-sixty/worktrunk#3993))

- **Relayed command output no longer prints ahead of the delayed-stream progress line**, and delayed-stream output routes through anstream so color is stripped consistently when redirected. ([#4037](max-sixty/worktrunk#4037), [#3995](max-sixty/worktrunk#3995))

- **A hook filter that matches nothing names the unconfigured source**, rather than reporting no match without saying which source was empty. ([#3997](max-sixty/worktrunk#3997))

### Documentation

- **`wt hook` says that `post-*` hooks from the two sources run concurrently**, so dependent commands belong in one source; `wt merge` records that `post-commit` can't run when the merge removes the worktree it anchors on. ([#4020](max-sixty/worktrunk#4020), [#4026](max-sixty/worktrunk#4026))

- **`wt step` help pages describe what the commands actually do**: `wt step prune` says it removes br

See merge request: Harmonybrew/homebrew-core!18763
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.

2 participants