mirror of
https://github.com/max-sixty/worktrunk.git
synced 2026-09-14 20:00:38 +08:00
codex/remove-codex-cloud-specific-tests-tn31ip
78 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
79be81a4bb | fix(merge): Accept merge-shaped fast-forward histories (#3509) | ||
|
|
85c9241cb1 |
Name the template owner in the -v variables block (#3495)
## The question
When `wt` shows a `template variables:` block under `-v`, does it say
which template the variables belong to? Mostly no — and in one case it
genuinely can't be worked out from the header.
## What was there
Four sites print a variables listing, and none named its owner in the
header:
| Site | Header | How you'd tell what it's for |
|---|---|---|
| foreground hook (`command_executor.rs`) | `template variables:` | the
`hook_type` row at the bottom of the table, or the `Running …` line
below it |
| background hook (`hooks.rs`) | `template variables:` | same — but see
below |
| alias (`alias.rs`) | `template variables:` | only the `Running alias
greet` line below it |
| `wt step eval` (`eval.rs`) | `Available template variables` |
unambiguous (one template, named on the command line) |
The adjacent blocks in the same `-v` lane already label themselves — `○
eval source`, `○ user:noop result` — so the variables table was the odd
one out in its own family.
Background hooks turned that from inconvenient into ambiguous. `wt -v
remove <branch>` prints one table per hook type back to back, then a
*single* combined `Running post-remove: cleanup (user); post-switch:
notify (user)` line — two identical headers over two ~20-row tables that
differ only in their rows.
## The change
Prefix each header with the hook type, alias name, or `eval`.
```
- ○ template variables: ○ post-remove template variables:
- ○ template variables: ○ post-switch template variables:
```
`wt step eval -v` now reads as one labeled family:
```
○ eval template variables:
branch = feature/auth-oauth2
○ eval source
{{ branch }}
○ eval result
feature/auth-oauth2
```
`eval`'s header was reworded from `Available template variables` for the
same shape; it was never ambiguous, but it's the fourth member of the
family and now matches the `source` / `result` headers directly beneath
it.
Display-only — no logic changed. Snapshots and the generated `step.md`
pages regenerated; `cargo run -- hook pre-merge --yes` green (4461
tests).
> _This was written by Claude Code on behalf of Maximilian Roos_
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
568b6de85f |
docs: consolidate duplicated explanations and trim slop (#3494)
Sweep of the docs for repetition and filler, from an audit of the hand-authored pages, the command pages' source in `src/cli/mod.rs`, and the plugin skill. Net −574 lines; every cut either had a surviving canonical home or restated an adjacent sentence. **One home per mechanism** (other mentions now link to it): - `template-append` — the LLM commits guide owns the rendering mechanism; the user- and project-config sections keep the key, an example, and what is unique to them (the project approval gate and the only-`template-append`-from-project scoping). Was explained in full in three places. - `-vv` diagnostic files — `wt config state logs` owns the four file descriptions; the FAQ summarizes in one sentence and links. - fsmonitor/trash cleanup — the FAQ's two overlapping `wt remove` bullets merge into one that distinguishes own-daemon teardown from the orphan sweep; troubleshooting.md's restatement compresses to a pointer, keeping its unique wedged-daemon-on-live-worktree guidance. - copy-ignored built-in excludes — the `wt step copy-ignored` page owns the directory list (previously enumerated 4×); the config sections state the rule and link. - hook-types table — `wt hook` owns it; the extending guide replaces its verbatim copy with a sentence. - LLM tool commands — unchanged, deliberately: the apparent hand-synced duplication between llm-commits.md and the config example is already machine-pinned by `test_llm_docs_commands_match_config_example`, so it cannot drift. **Cut-over debt**: the deprecated `wt config state ci-status` section shrinks to a deprecation pointer — its status table, fetch order, and caching notes all duplicated the `wt list` CI-status section. **Structure**: `wt step copy-ignored`'s "Features" list dissolves into the sections that owned its facts (excludes → "What gets copied", reflink → "Performance"); the four trailing "Note: This command is experimental…" lines go (the `[experimental]` badge already appears twice per section); shell-integration.md described the directive-file mechanism twice and now describes it once. **SKILL.md** drops from 339 to 181 lines: the permission-model section duplicated the config-types bullets, the hook-type mapping appeared twice, and the "Determining Which Config to Use" / "Validation Before Adding Commands" scaffolding enumerated judgments an agent makes on its own. The approvals-escalation and agent-handoff sections are untouched. **Deliberately not addressed**: `wt list`'s schema 1 JSON tables (still the default schema; the wholesale deletion lands when the default flips) and corpus-wide em-dash density (a house-style decision, not a per-page fix). Generated mirrors (docs command pages, skill references, plugins mirror, `dev/*.example.toml`, help snapshots) regenerated via `test_docs_are_in_sync` and `cargo insta`. > _This was written by Claude Code on behalf of max_ Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
e459e3e6c2 |
fix(plugin): WorktreeRemove hook exits 0 when the worktree is already gone (#3493)
## Summary Fixes #3488. Claude Code (>= 2.1.211) auto-fires the plugin's `WorktreeRemove` hook on session teardown for the recorded worktree path — but that worktree may already be gone (removed by `wt merge` after a PR, or a manual `wt remove`). In that case `wt remove <gone-path>` resolves the missing path to a *branch-only* target and fails with `✗ No branch named <path>` (exit 1). Claude Code treats a non-zero `WorktreeRemove` as a failed removal and keeps the session row (`worktree could not be removed (WorktreeRemove hook failed)`), so the completed background session can no longer be deleted with Ctrl+X in the `claude agents` view. Per @max-sixty's call on the issue, this fixes it directly in the hook rather than adding a `--if-exists` flag (protected CLI surface) or punting to Claude Code. ## Change One guard in the `WorktreeRemove` hook command: ```diff - ... cd "${CLAUDE_PROJECT_DIR:-.}" || exit 1; bash ".../wt.sh" remove --foreground "$p"' + ... cd "${CLAUDE_PROJECT_DIR:-.}" || exit 1; [ -e "$p" ] || exit 0; bash ".../wt.sh" remove --foreground "$p"' ``` If the recorded worktree path no longer exists on disk, the hook is a no-op (exit 0). This is the narrowed form of the issue's Option 1: leniency is scoped to "the worktree is already gone", not a blanket success. When the path still exists, `wt remove` runs exactly as before, so genuine failures (dirty / locked / unmerged branches — the #2939 spirit) still surface loudly and are never masked. It also doesn't swallow a typo, since a typo'd path doesn't exist on disk only when it also doesn't exist as a worktree — and Claude Code only ever fires this hook with a path it recorded, not user input. The guard lives inside the single-quoted `bash -c '...'` body, so it doesn't touch outer login-shell (fish/zsh/bash) parsing, and it adds no `-D`/`--force-delete`. ## Testing - `test_claude_hook_commands_parse_in_all_shells` (fish/zsh/bash parse) — passes - `test_plugin_layout_is_consolidated` (includes the "no `-D`/`--force-delete`" assertion on the WorktreeRemove command) — passes This is orthogonal to #3489, which already landed the cwd (`CLAUDE_PROJECT_DIR`) anchoring. Co-authored-by: worktrunk-bot <worktrunk-bot@users.noreply.github.com> |
||
|
|
5ebdcb1500 | fix(plugin): anchor WorktreeRemove at the project dir (#3489) | ||
|
|
d80cc80ebd | feat(config): flag stale default-branch cache in wt config state (#3471) (#3478) | ||
|
|
bb4abca109 |
fix: anchor relative WORKTRUNK_PROJECT_CONFIG_PATH to the worktree root (#3460)
`WORKTRUNK_PROJECT_CONFIG_PATH` was returned verbatim, so a relative value resolved against the process cwd while the default `.config/wt.toml` is anchored to the worktree root. Running `wt` from a subdirectory made a relative override silently no-op (or read an unintended file). This is the config-path footgun from #3454 (the RFC there is a separate discussion). A relative override now resolves against the same worktree root as the default (current worktree, or the primary worktree at a bare root), so it behaves identically from any subdirectory. Values that can't be anchored fail loudly instead of guessing: - relative with no worktree root to anchor to (bare repo with no linked worktrees) errors - Windows forms that are neither fully absolute nor relative (drive-relative `C:cfg`, rooted-but-driveless `\cfg` or `/tmp/x`) error rather than resolving against the process drive or cwd Absolute values are unchanged (returned verbatim, no git calls), which keeps the test-isolation use of the var working. An empty value still means no project config, as before. `wt config show` (human format) and `wt config update` previously folded a `project_config_path()` error into "not in a git repository" / a silent skip; they now propagate it, matching the JSON path. Merging main brought in #3462's committed default-branch config fallback for bare repos whose primary worktree is parked off the default branch. That fallback now stands down when `WORKTRUNK_PROJECT_CONFIG_PATH` is set: an explicit override names the config source outright (a missing file there means no project config), and the override exists for test isolation, where reading the repo's own committed config is exactly the leak being prevented. The human format of `wt config show` now reports "No project config" when no location exists (bare repo with no worktrees, empty override) instead of the inaccurate "Not in a git repository", which stays reserved for actually being outside a repo. Compatibility: invoked from the worktree root, cwd and root coincide, so old and new resolution agree. From a subdirectory the old resolution found nothing, which is the bug being fixed, so no deprecation shim is included. Thanks @indexzero for reporting in #3454. 🤖 Generated with [Claude Code](https://claude.com/claude-code) > _This was written by Claude Code on behalf of max_ --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
c4673bfb93 |
fix(plugin): anchor WorktreeCreate at the project dir (#3453)
**Anchor the `WorktreeCreate` hook at `CLAUDE_PROJECT_DIR`.** The hook
command inherits the session shell's working directory. When the shell
has `cd`'d outside the repository (e.g. into a reference-papers
directory elsewhere on disk), `wt switch` dies with *not a git
repository* and every isolated-agent spawn in the session fails —
observed as a six-agent spawn failure mid-session. `cd
"${CLAUDE_PROJECT_DIR:-.}"` pins the hook to the repository the session
was launched in, falling back to the current behavior when the variable
is unset.
_Originally this PR also added `--base=@` to base agent worktrees on the
session's HEAD. That change was dropped at the maintainer's request
(worktrunk-bot pushed the removal) so it can be evaluated separately —
hardcoding `--base=@` would override the native `worktree.baseRef`
setting for everyone, and the hook has no way to read that setting. This
PR is now the `cd` anchor only._
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: silvanshade <silvanshade@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
|
||
|
|
a6972e4be0 |
fix(shell): detect the current shell from the process tree, not $SHELL (#3455)
`wt switch` could tell a fish user "shell requires restart" because `$SHELL` (the login shell, say zsh) had the integration line while the shell actually in use didn't. `current_shell()` keyed everything on `$SHELL`, which names the login shell rather than the shell wt was invoked from. **Process-tree detection.** `shell::ancestor_shell()` (src/shell/utils.rs) walks up from wt's parent to the nearest enclosing shell: Linux reads `/proc/<pid>/stat`; macOS parses one `ps -Ao pid=,ppid=,comm=` snapshot. macOS cannot use per-pid `p_comm` (libproc/sysctl): `/bin/sh` re-execs bash via `/var/select/sh`, so an sh script's `p_comm` reads "bash", while `ps`'s comm preserves argv[0]. Verified live and pinned by `test_probe_reports_invoked_name_for_sh`. The walk passes through non-shell wrappers (`git`, `sudo`) and plain script interpreters (`sh`, `dash`); a known-but-unsupported interactive shell (tcsh, ksh) stops the walk so a shell further up can't claim the session. `current_shell()`/`current_shell_name()` consult the walk first and fall back to `$SHELL` / `PSModulePath` (Windows behavior unchanged; Git Bash sets `$SHELL` itself). **Hedged messaging.** The reason string is now "shell integration installed but not active", and the hint presents restart as the likely fix with an escape hatch: "A shell restart usually activates shell integration; if it doesn't, ask an agent to debug with the docs @ https://worktrunk.dev/llms.txt" (the LLM docs index works for any agent; the `/worktrunk` skill exists only for plugin users). Post-install hints are unchanged (restart advice there is unconditional by construction). `wt config show` now prints `Detected shell: <name> (process tree)` next to `$SHELL` in the diagnostic gutter. **Also fixed along the way:** `shell_from_name` requires a word boundary, so `fishd`/`bashtop`/`numactl` no longer classify as shells (`numactl` previously matched Nushell via the bare `nu` prefix); the post-install/uninstall restart hints compare `Shell` values instead of display strings, which never matched process-tree names like `pwsh` or `zsh-5.9`. **Testing.** Integration tests spawn wt under a test harness whose real ancestry (nextest, cargo, the developer's shell) would leak in, so `WORKTRUNK_TEST_PARENT_SHELL` is pinned empty in `STATIC_TEST_ENV_VARS` (and in the two PTY helpers that mirror it by hand, which CI round 1 caught: locally the walk found the dev zsh, on CI the runner's bash) and set per-test to simulate ancestors. New integration tests cover the three detection outcomes (mismatch suppresses restart advice, configured ancestor shows the hedged hint, unsupported ancestor overrides `$SHELL`); unit tests cover the classifier, the walk itself (injectable process-table lookup: wrapper pass-through, transparency, cycle/depth/init terminations), and both OS probes against live processes; `wt config show` is snapshot-tested with an override ancestor and run once unpinned so the production walk executes end-to-end. Known limits, accepted: the walk stops at the nearest supported shell even when it's a non-interactive script interpreter (`bash -c` wrapping wt reports bash), hence the hedged wording; the walk is bounded at 16 hops and any unreadable hop falls back to `$SHELL`. > _This was written by Claude Code on behalf of max_ 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
36ba57bdca |
fix(plugin): ship skills to Codex installs via a generated real-file mirror (#3440)
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> |
||
|
|
0fc3518f8d |
docs(plugins): record the deliberate absence of a manifest version field (#3434)
Follow-up to #3431: records in `plugins/worktrunk/CLAUDE.md` why the Claude manifest deliberately omits a `version` field — installs pin the marketplace git SHA, so a semver would be a second version to maintain with nothing consuming it — so a future cleanup pass doesn't "fix" the `claude plugin validate` warning by adding one. > _This was written by Claude Code on behalf of max_ Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
6c122a5d08 |
refactor(plugin): adopt Claude's canonical manifest layout, convention-only discovery (#3431)
## What this does Moves the Claude plugin manifest to the canonical `plugins/worktrunk/.claude-plugin/plugin.json` and strips it to metadata only, dropping the `hooks` and `skills` keys so that both component types load purely by Claude Code's convention discovery. Rewrites the layout docs and inverts `test_plugin_layout_is_consolidated` to pin the new invariants. ## Background The plugin layout has been in flux: [#3382](https://github.com/max-sixty/worktrunk/pull/3382) Claude-scoped the hooks filename, which silently broke hook loading ([#3417](https://github.com/max-sixty/worktrunk/issues/3417)), and [#3418](https://github.com/max-sixty/worktrunk/pull/3418) reverted it. That revert restored working hooks but left two claims baked into the layout, the docs, and the test that this PR found to be stale. ## Research method Everything below was verified empirically against claude-cli 2.1.207 (the current release), not from docs alone: - **Scratch marketplace probe**: a throwaway marketplace mirroring worktrunk's exact shape (repo-root `.claude-plugin/marketplace.json`, plugin in a `./plugins/<name>` subdir source, `skills` as a symlink pointing outside the plugin dir, hooks at the conventional path) with two plugin variants — manifest in `.claude-plugin/plugin.json` vs. bare `plugin.json` at the plugin root — installed through the real `claude plugin marketplace add` + `claude plugin install` flow. - **Component inventory**: `claude plugin details` and `claude plugin validate` on both variants. - **Live sessions**: headless `claude -p` runs confirming skills appear in-session and hook commands actually execute (marker files, and reading the 🤖 activity marker from inside a live session). - **Loader inspection**: the manifest resolution order read directly out of the claude-code binary's bundled loader code. ## Findings 1. **The "no `.claude-plugin/` wrapper" claim no longer holds.** The repo documented — and the test pinned — that Claude wants `plugin.json` bare at the plugin root, and that a `.claude-plugin/` wrapper in a subdir-source plugin fails with "Plugin not found". On 2.1.207 the wrapper form installs, loads skills, and fires hooks end-to-end. The loader tries `.claude-plugin/plugin.json` first with root `plugin.json` only as a runtime fallback, several marketplace-resolution paths read *only* the wrapper, and `claude plugin validate` rejects the root-level form outright ("No manifest found in directory. Expected .claude-plugin/marketplace.json or .claude-plugin/plugin.json"). 2. **"Claude has no skill auto-discovery" is wrong.** Skills are auto-discovered by scanning `skills/*/SKILL.md`; a `skills` manifest array only *adds* directories to that scan (entries equal to the default dir are filtered). An unlisted skill behind the out-of-plugin symlink loaded fine — the installer dereferences the symlink into a real directory in the install cache. Dropping the array also makes the old test guard ("every skill must be listed or it's silently invisible") structurally unnecessary: a new repo-root skill now ships to Claude, Codex, and Gemini with no manifest change anywhere. 3. **The `hooks` manifest key was dead config.** #3417 established the string-path override isn't honored for plugin loads; convention discovery of `hooks/hooks.json` is the mechanism that works. A key pointing at the conventional path adds nothing and could mask a future mislocated file, so it's gone (one mechanism per guarantee). ## Changes - `plugins/worktrunk/plugin.json` → `plugins/worktrunk/.claude-plugin/plugin.json`; content reduced to `name`/`description`/`author`. - `plugins/worktrunk/CLAUDE.md`: layout diagram and Claude path-resolution bullet rewritten to the convention-only model; the refuted wrapper claim and the stale no-auto-discovery claim removed. - Root `CLAUDE.md`: the Plugin Layout pointer updated (dropped the stale "Codex no-hooks re-enablement" phrase — Codex hooks shipped inline in #3364). - `test_plugin_layout_is_consolidated`: now asserts the wrapper manifest exists, a root-level `plugin.json` does not, the manifest carries no `hooks`/`skills` keys, the plugin `skills` symlink resolves to the repo-root `skills/` (unix only — Windows checkouts without `core.symlinks` materialize it as a file), and every repo-root skill dir carries a `SKILL.md` (the scan silently ignores a dir without one). Codex and Gemini are untouched: Codex reads only its `.codex-plugin/` wrapper, and its inline `hooks` object still overrides Codex's own convention discovery, so the #3362 collision stays closed independent of any of this. ## Verification - `claude plugin validate plugins/worktrunk` passes (it failed on the old layout). Remaining warnings are deliberate: no `version` field (installs pin the git SHA as the version; a hand-maintained semver would go stale) and the plugin-root `CLAUDE.md` (developer docs, not plugin context). - A live `claude --plugin-dir` session on this exact tree loads both skills and the `UserPromptSubmit` hook sets the 🤖 marker, observed from inside the session. - The scratch-marketplace probe covered the same shape through the full marketplace install flow, including the symlinked skills dir. - Full pre-merge gate: 4389 tests passed plus lints, doctests, and docs-sync. ## Risk The one scenario this could regress is a Claude Code version old enough to read *only* the root-level manifest. That couldn't be tested directly; the mitigations are that the wrapper is the documented convention, the loader prefers it today, and Claude Code self-updates aggressively. The old layout's "verified end-to-end" claim most likely dates from an earlier 2.1.x behavior or a misdiagnosed failure. > _This was written by Claude Code on behalf of max_ Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
69d6afb1fe |
fix: restore Claude plugin hooks to conventional hooks/hooks.json (#3418)
## Problem Since [#3382](https://github.com/max-sixty/worktrunk/pull/3382) ("Claude-scope the shared hooks payload"), the Claude plugin's hooks stopped loading in Claude Code. Skills and commands still load, so the plugin looks healthy — but `/hooks` shows none of worktrunk's handlers and the 🤖/💬 activity markers never update. It fails silently. Root cause (confirmed by the reporter on Claude Code 2.1.207): Claude Code discovers plugin hooks by **convention** at `hooks/hooks.json`. It does **not** honor `plugin.json`'s string-path `"hooks": "./hooks/claude-hooks.json"` override for plugin loads. The #3382 rename moved the file off the only path Claude's loader actually reads. The rename was intended to stop Codex's convention discovery from surfacing Claude's events ([#3362](https://github.com/max-sixty/worktrunk/issues/3362)). But the Codex manifest already defined its hooks **inline** (`Some(Inline)` branch of `resolve_manifest_hooks`), which overrides Codex's convention discovery — and that inline definition **predates** the rename (verified against `2677f05^`). So the rename was always redundant for the Codex scoping, while it broke Claude's discovery. ## Solution Restore the file to the conventional `plugins/worktrunk/hooks/hooks.json` and point `plugin.json`'s `hooks` back at it. `plugin.json` reverts byte-for-byte to its pre-#3382 state. This does not resurface the #3362 collision: the Codex manifest's inline `hooks` object keeps Codex off the shared `hooks/hooks.json` regardless of the filename. The two toolchains now coexist on one file — Claude discovers it, Codex ignores it via the inline override. The `test_plugin_layout_is_consolidated` assertions covering the Codex inline manifest are unchanged and still pass. Also updates `plugins/worktrunk/CLAUDE.md` (layout diagram + Known Limitations) and drops the now-reverted #3382 CHANGELOG entry, replacing it with a Fixed entry (both were in the unreleased 0.67.0, so the net change is nil). ## Testing Reproduction is captured in `test_plugin_layout_is_consolidated` (`tests/integration_tests/config_show.rs`), whose invariant was inverted from "the Claude file must be Claude-scoped" to "the Claude file must sit at the conventional `hooks/hooks.json` that Claude Code's loader discovers." Against pre-fix production it fails (`left: "./hooks/claude-hooks.json"`); after the rename it passes. - `test_plugin_layout_is_consolidated` ✅ - `test_claude_hook_commands_parse_in_all_shells` ✅ (reads the renamed file; shell-integration-tests) - Full `integration_tests::config_show` module: 136 passed ✅ - `test_docs_are_in_sync` ✅ **Not verified end-to-end from CI** (per running-tend "Don't ship fixes you can't verify"): the bot cannot drive a live Claude Code or Codex session to observe the hooks firing. The fix rests on the reporter's confirmed symptom + repro (symlinking `hooks/hooks.json` → `claude-hooks.json` immediately restores the hooks) and on Claude Code's [documented convention path](https://code.claude.com/docs/en/plugins-reference.md) (`hooks/hooks.json` in plugin root). The Codex-stays-clean claim rests on the existing code analysis in CLAUDE.md and the inline manifest predating the rename, not a live Codex run. --- Closes #3417 — automated triage --------- Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> |
||
|
|
2677f05d0c | feat(plugin): Claude-scope the shared hooks payload (#3382) | ||
|
|
512a94de2f | feat(plugin): ship Codex-native activity hooks (#3364) | ||
|
|
33deef94c7 | docs: fix stale marker config key and bare-repo test comment (#3066) | ||
|
|
11129d3811 |
fix(plugin): fail worktree hooks before side effects; document path args (#3060)
Hardens the plugin's worktree-lifecycle hooks against malformed
payloads, and documents two things they rely on.
**Hooks** (`plugins/worktrunk/hooks/hooks.json`): in the pipeline form,
`jq -r .name | xargs … wt switch --create {} …` runs `wt` with whatever
jq printed — so a payload missing `.name` minted a real branch named
`null`, and `set -o pipefail` could only report the failure after the
side effect (verified under `/bin/sh`). Both hooks now validate the
field in a command substitution before `wt` runs (`name=$(jq -er .name)
|| exit 1; …`), which fails with nothing created and stays
whitespace-safe via quoted variables. The `bash -c` wrapper remains —
hook commands must parse under fish/zsh/bash and fish rejects
`name=$(…)` — but `set -o pipefail` is gone: the only remaining pipe
ends in `jq -er .path`, whose exit is the pipeline's. Exercised under
`sh -c` against the shipped JSON: missing field → exit 1, no branch;
fresh create → path on stdout, exit 0; existing branch → wt's real
error, nonzero; remove by path → removed.
**Docs**: `wt remove`'s positional also accepts worktree paths
(`resolve_worktree_arg` tries branches first, then paths) and the
`WorktreeRemove` hook passes a path — the help line now reads "Branch
name or worktree path". The plugin README lists `jq` as a hook
dependency, and the skill's branch-naming step asks for names consistent
with the repo's existing worktrees.
> _This was written by Claude Code on behalf of max_
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
fd3ae5bff4 |
feat(plugin): optional branch name and single-route flow for /wt-switch-create (#3058)
The `/wt-switch-create` branch argument becomes optional (a name is
picked from the task when omitted), and the skill's workflow is rebuilt
from a guard-heavy three-route flow into one route plus one error-driven
fallback.
**The new flow**: pick a branch name if none was given, create the
worktree with `wt -C <repo> switch --create <branch> --no-cd
--format=json` in Bash (rerunning without `--create` when the user named
an existing branch), re-root the session with `EnterWorktree({path})`,
and on a graceful rejection (different repo, pinned cwd, nested session)
work in the worktree via absolute paths instead.
**Why the rewrite**: the old flow's guards encoded hypotheses about
Claude Code that turned out wrong or untested. The load-bearing
discoveries, each verified by binary inspection, official docs, or live
tests and documented in the new `skills/wt-switch-create/rationale.md`:
`EnterWorktree({path})` accepts worktrunk's sibling layout on first
entry; every rejection it can raise is side-effect-free, so
try-then-fallback replaces prediction; the previous
`EnterWorktree({name})`/hook route silently auto-removes a clean
worktree at session exit, which is wrong for durable worktrunk worktrees
(path-entered worktrees persist); and the "pinned cwd" the old guards
defended against was a misdiagnosis of the harness resetting `cd` that
leaves the session's working directories.
**Hook fix**: both `WorktreeCreate` and `WorktreeRemove` pipelines are
wrapped in `bash -c 'set -o pipefail; ...'`. Without it the trailing
`jq` exits 0 on empty input, so a `wt` failure surfaced as a
"successful" hook with an empty path. The wrapper is explicit `bash`
because hooks spawn via `/bin/sh -c`, where dash rejects `set -o
pipefail` fatally. `xargs -I{}` also fixes worktree paths containing
spaces. Tested end-to-end in both directions (success exits 0 with the
path; failure exits 1 with empty stdout).
Docs, README, and plugin CLAUDE.md are aligned with the new mechanism.
The agent-isolation use of the `WorktreeCreate` hook is unchanged.
Review trail: the design survived an evidence audit (every rationale
claim independently re-verified), a code review (bare directory-named
tokens no longer parse as the repo; mid-session moves now stash
uncommitted work across), and a level check that considered and rejected
fixing this in `wt` itself (an idempotent `--create` cannot encode who
chose the branch name, which determines the correct recovery).
> _This was written by Claude Code on behalf of max_
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
a35dd7f598 | fix(plugin): add waiting-for-input marker transitions for Claude hooks (#3023) | ||
|
|
6fd1bf4566 | Fix Claude hooks for Fish shell users. (#2962) | ||
|
|
eaa230bc05 | fix(plugin): drop -D from WorktreeRemove hook to preserve unmerged branches (#2940) | ||
|
|
ec62580c29 |
revert(hooks): keep docs on pre-start/post-start; code accepts both (#2857)
Per @max-sixty's [direction in #2838](https://github.com/max-sixty/worktrunk/issues/2838#issuecomment-4509447593): revert the docs portion of #2840 and keep the code. Docs continue to recommend `pre-start`/`post-start`; both names work in code so anyone who already followed the briefly-changed docs (e.g. @EcksDy) isn't stranded once a release ships these aliases. ## User-visible — back to `pre-start`/`post-start` - README, docs site, skill mirrors, `dev/*.example.toml`, `plugins/worktrunk/README.md`, `flake.nix`, `.config/wt.toml` - `src/cli/mod.rs` / `src/cli/config.rs` / `src/cli/step.rs` / `src/help.rs` after_long_help and example snippets — and the auto-synced `docs/content/` and `skills/worktrunk/reference/` mirrors - `wt hook --help` canonical subcommand names; completion advertises `-start` only - `HookType` Display via strum, serde `rename`, and clap `ValueEnum` name — all `pre-start`/`post-start`. The Rust variant identifiers stay `PreCreate`/`PostCreate` (internal; we already paid for that rename in #2840, and now the eventual flip is a Display-only change) - `HooksConfig` serde canonical fields ## `*-create` still works (kept code) - `wt hook pre-create` / `post-create` — CLI alias on the canonical subcommand - `pre-create` / `post-create` in config: top-level, `[hooks.*]`, and per-project, in string, `[table]`, and `[[array-of-tables]]` form. Mechanism: serde `alias = ...` on the field, plus a silent in-memory rename in `migrate_content()` so the round-trip in `unknown_tree` doesn't flag table forms as schema-unknown. - The pre-0.32.0 `post-create` fatal-load-error machinery stays removed — the name is reclaimed, and both forms load without error. ## Smaller bits - `valid_user_config_keys()` / `valid_project_config_keys()` append `pre-create` / `post-create` so the unknown-field round-trip skips them. `test_valid_*_keys_all_deserialize` skips both aliases (they can't sit alongside the canonical without a duplicate-field error). - `DEPRECATED_SECTION_KEYS` drops the `pre-start`/`post-start` entries #2840 added — `pre-start`/`post-start` are canonical again. - `find_pre_start_from_doc` / `find_post_start_from_doc` / `find_renamed_hook_key` / `is_non_empty_item` / `migrate_start_hooks_doc` and their tests are removed; the migration direction flips via a new `migrate_create_hooks_doc` (silent, mirrors the prior shape). - Test files `e2e_shell_post_create.rs` and `post_create_commands.rs` rename back to `_post_start_` (via `git mv`, so the rename shows as a rename). ## Testing `cargo run -- hook pre-merge --yes` — 3806 tests pass; the 10 failures are all `case_4` of `shell_wrapper::unix_tests::*` (nu-shell case; `nu` isn't installed in this runner; same failures occur on `main`). Also manually verified that a fresh `wt switch --create` against a project config with `[post-create]` loads cleanly with no unknown-field warning and the hook fires as `post-start`. ## Follow-up Per @max-sixty: in a couple of weeks, once a release with both-names-work is out and users have had a chance to upgrade, the docs flip is straightforward (most of it is in `src/cli/mod.rs`'s `after_long_help` and the doc-sync test propagates). Re #2838. Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
d7e3f88422 |
feat(hooks): rename worktree-creation hooks to pre-create/post-create (#2840)
Phase 1 of the staged hook rename tracked in #2838: the worktree-creation hooks `pre-start`/`post-start` become `pre-create`/`post-create`. The old names keep working with no deprecation warning yet (Phase 2, months out, adds the warning). ## What changes - `pre-create`/`post-create` are canonical everywhere: the `HookType` enum, the `HooksConfig` serde fields, the `wt hook` CLI, completion, and all docs. - Old names keep working: `migrate_content()` rewrites `pre-start`/`post-start` config keys to `-create` before serde, and `parse_hook_type` accepts the old CLI names as silent aliases. `wt config update` rewrites them on disk; `wt config show` shows the migration diff. `wt hook <type>` execution and `wt hook show` both accept the old names; completion and `--help` advertise only the canonical names. - `detect_deprecations()` flags the old keys so `update`/`show` act on them, but `format_deprecation_warnings()` stays silent (Phase 2 adds the warning). A new empty-warnings guard in `check_and_migrate` keeps a `-start`-only config from emitting a stray hint. - The dead pre-0.32.0 `post-create` machinery is removed: the fatal `POST_CREATE_REMOVED_MSG` load error, the vestigial `HooksConfig.post_create` merge-fold, and `find_post_create_from_doc`. `post-create` is reclaimed as the canonical background creation hook. ## Semantic flip Before v0.32.0, the key `post-create` named a *blocking* hook. It now names the *background* one. Since v0.44.0, a pre-0.32.0 `post-create` config is a fatal load error on the `check_and_migrate` paths: `ProjectConfig::load` and user/system config loading, which fire on essentially every `wt` command. A repo carrying one has been unusable ever since. The one path that skips that check is `project_config_at_ref` (the base-ref read behind `wt switch --create`), which applies only structural migration. A pre-0.32.0 `post-create` surviving solely on a base ref, never checked out into a worktree, would now load as a background hook rather than folding into the blocking `pre-start`. That edge case is accepted: once `post-create` is valid again, reclaiming the name and detecting the dead key are mutually exclusive. ## Reviewing this diff 205 files, but the substance is ~36 files under `src/`. The rest is regenerated snapshots and auto-synced doc mirrors. Start with: - `src/config/deprecation.rs` — detection (`find_renamed_hook_key`), migration (`rename_hook_key`), removal of the fatal block, the empty-warnings guard, and the `DEPRECATED_SECTION_KEYS` entries that stop unknown-field detection from flagging the migrated keys. - `src/config/hooks.rs`, `src/git/mod.rs` — the serde field and enum renames. - `src/config/project.rs` — `ProjectConfig::load` deserializes `check_and_migrate`'s migrated content, so a current-worktree config using the old keys loads into the canonical fields. - `src/cli/hook.rs`, `src/commands/hook_commands.rs`, `src/completion.rs`, `src/main.rs` — the CLI alias layer; `wt hook show` accepts the old type names as hidden value-parser aliases. - `src/cli/mod.rs` — the `wt hook` docs, including the soft-deprecation note linking #2838. The ~93 modified snapshots also pick up deterministic env-block lines (`GIT_*: ""`, `LLVM_PROFILE_FILE`) that pre-existing snapshots already carry. That is stale-snapshot drift surfaced by the regeneration, not a behavior change. ## Testing Full suite green (3799 tests). New coverage: `snapshot_migrate_start_to_create` (migration preserves value shape and position), `test_deprecated_start_hook_key_runs_silently` and `test_standalone_hook_start_alias_runs_silently` (old config and CLI names run with no warning), `test_config_show_displays_start_hook_migration` (`config show` reveals the diff without an "unknown field" warning), and `test_hook_show_accepts_deprecated_start_hooks` (a current-worktree config using the old keys loads, and `wt hook show` takes both the canonical and the deprecated type arguments). Part of #2838. 🤖 Generated with [Claude Code](https://claude.com/claude-code) |
||
|
|
1cc89d172e |
docs(claude): slim root CLAUDE.md, relocate detail to sibling files (#2816)
The root `CLAUDE.md` loads into every conversation, so its ~510 lines were paid on every session. This cuts it to ~190 lines without losing any documented behavior or invariant. Three things happened: derivable/stale content was removed (the JSON Output Format pointer, just a redirect to `wt list --help`), prose was tightened throughout, and task-specific detail was pushed down to the sibling `CLAUDE.md` that loads where that work happens — the root keeps a one-line pointer. **Relocations** (substance preserved at the destination, root keeps a pointer): - Testing commands, the `mock-stub` filtered-run gotcha, Claude Code web setup, the shell/PTY nextest SIGTTOU suspension, and codecov investigation mechanics → `tests/CLAUDE.md` - Doc sync taxonomy (the three categories + PRIMARY SOURCE), three-context help-text authoring, the `.gitattributes` `linguist-generated=false` exemption, config-TOML double-comment rule → `docs/CLAUDE.md` - Plugin layout, the Codex no-hooks re-enablement conditions, the accepted `wt-switch-create` tradeoff → `plugins/worktrunk/CLAUDE.md` (its back-pointers into the root were made self-contained so nothing dangles) - The add-a-CLI-command recipe → `src/commands/CLAUDE.md` **Kept in the root, tightened but substance intact:** the governing invariants — Data Safety, the Command Execution Principles (`shell_exec::Cmd`, structured output, network policy, signal handling), Project Commands Run Only After Approval (verbatim — the #2806 TOCTOU fix depends on its wording), the codecov merge gate, Config Deprecation, accessor naming, and the Worktree Model. A third commit consolidates the doc-sync explanation in `docs/CLAUDE.md`: the new "Doc sync taxonomy" section was overlapping with the file's existing "Command documentation" area on "edit `src/cli/mod.rs`" and `test_docs_are_in_sync`. Now the taxonomy is the single entry point and "Command page generation" reads as the category-1 mechanism details. Every CLAUDE.md heading quoted in a code comment is preserved verbatim so the breadcrumbs still resolve under a case-sensitive grep: `Plugin Layout` (`src/commands/config/codex.rs`), `Project Commands Run Only After Approval` (`src/commands/hook_plan.rs`, `src/commands/picker/mod.rs`), `Network Access` (`src/git/repository/config.rs`), and `Signal Handling` (`src/git/error.rs`, `src/commands/run_pipeline.rs`, `src/commands/command_executor.rs`). The first push downcased the latter two; the automated reviewer caught it and the second commit restored Title Case across all Command Execution Principles subheadings. `test_docs_are_in_sync` passes (docs-only change; no code touched). --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
d7e33efc6c |
refactor(plugin): relocate Gemini extension to repo root (#2807)
Follow-up from a full review of the plugin implementation.
**The finding:** Gemini hard-probes `${extensionPath}/{hooks,skills}/`
at the extension root with no manifest path indirection, and `gemini
extensions install owner/repo` reads `gemini-extension.json` only at the
clone root. The `plugins/gemini/` sibling dir fought that — it needed a
`skills` symlink and a byte-identical bundled `wt.sh` purely to survive
`gemini extensions install`'s `fs.cp`, and github-URL install was broken
entirely (manifest invisible under a subdir). One install-fragile
artifact (`wt.sh`) was bundled to solve this; the other (`skills`) was
left a symlink that the guard test actively pinned — so the extension
shipped no skills to real users.
**The fix:** treat Gemini's manifest as the loader-mandated root pointer
it is — the third tool pointer alongside Claude's
(`.claude-plugin/marketplace.json`) and Codex's
(`.agents/plugins/marketplace.json`). With the manifest at the repo
root, `${extensionPath}` is the repo root, so `${extensionPath}/skills/`
resolves to the real single-sourced repo-root `skills/` and the hooks
call the canonical `${extensionPath}/plugins/worktrunk/hooks/wt.sh` by
its real path. This is net deletion: the symlink, the duplicated
`wt.sh`, and its drift guard all go away, and native `gemini extensions
install owner/repo` now works.
Verified with `gemini extensions validate .` against gemini-cli 0.42. A
true end-to-end `gemini extensions install` was not exercised — it
blocks on an interactive trust prompt — so the native-install claim
rests on documented Gemini behaviour plus `validate`, not a live
install.
Also hardens `test_plugin_layout_is_consolidated` (two other review
findings):
- Every repo-root `skills/` subdir must be listed in the Claude
`plugin.json` `skills` array. Claude has no skill auto-discovery
(confirmed against the official plugin reference), so an unlisted skill
is silently invisible to Claude while Codex/Gemini pick it up — this
turns that latent drift into a build failure.
- All four manifest descriptions (Claude ×2, Codex, Gemini) must share
the canonical product opening sentence. Byte-identical across all four
is schema-impossible (Codex omits the activity clause, Gemini says
"extension"), but the shared stem is a meaningful cross-tool drift
guard.
`CLAUDE.md` and `plugins/worktrunk/CLAUDE.md` updated: the repo root now
carries one loader-mandated pointer per tool (three, not two); tree
diagram, path-resolution rules, and the `wt-switch-create`
shared-`skills/` tradeoff all extended to Gemini.
Full pre-merge gate green locally: 3745 tests, 0 skipped; all lints; no
snapshot churn.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
d22917702e |
refactor(plugins): consolidate Claude + Codex into one payload dir (#2789)
The Claude plugin lived at the repo root (`.claude-plugin/`) while the
Codex plugin lived in `plugins/worktrunk/` — two homes for one logical
plugin, with the description string duplicated across both and drifting
independently. This collapses them into a single payload directory.
**What moved.** `git mv
.claude-plugin/{plugin.json,hooks/,CLAUDE.md,README.md} →
plugins/worktrunk/` (history-preserving renames). The repo root keeps
only the two marketplace pointers — Claude and Codex each hardcode their
marketplace path with no fallback, so two pointer files is the
irreducible floor. Both pointers' `source` now resolves to
`./plugins/worktrunk`. `.claude-plugin/` is now a single 2-line file.
**The load-bearing constraint** (verified live against claude-cli
2.1.x): for a *subdirectory* `source`, Claude expects `plugin.json` at
the plugin root **without** a `.claude-plugin/` wrapper — that wrapper
is marketplace-root-only. (First attempt with the wrapper failed `Plugin
not found`; the corrected layout installs cleanly.) Codex keeps its own
required `.codex-plugin/` wrapper. So inside `plugins/worktrunk/`:
`plugin.json` is Claude's, `.codex-plugin/plugin.json` is Codex's,
`hooks/` is Claude's, `skills → ../../skills` is shared.
**Path edits inside moved files:** `plugin.json` `hooks` →
`./hooks/hooks.json`; `hooks.json`
`${CLAUDE_PLUGIN_ROOT}/.claude-plugin/hooks/wt.sh` →
`${CLAUDE_PLUGIN_ROOT}/hooks/wt.sh`.
**Reviewer navigation:** `git log` shows the moves as renames.
`plugins/worktrunk/CLAUDE.md` was rewritten to document the unified
layout + per-tool path resolution; the repo `CLAUDE.md` "Plugin Layout"
section likewise. `tests/integration_tests/config_show.rs` adds
`test_plugin_layout_is_consolidated`, which locks the layout invariants
(`.claude-plugin/` is marketplace-only, Claude manifest at plugin root,
hooks relative to it) **and** asserts the duplicated Claude description
stays byte-identical across the marketplace pointer and the manifest —
that was the standing follow-up; JSON can't `include!`, so a drift test
is the right tool, not a generator.
**Verification:** Live end-to-end on both real CLIs, twice — once with
copied skill dirs, once mirroring the real tree *including the
`plugins/worktrunk/skills` symlink* (Claude's `skills` array resolves
through it). Full pre-merge gate green: 3720 passed, 0 skipped, all
pre-commit lints, no snapshot churn. No user-facing docs/help text
changed (the consolidation is repo-internal), so `test_docs_are_in_sync`
needed no resync.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
233cd493d0 |
fix(codex): drop activity-marker hooks Codex can't drive (#2786)
The Codex plugin shipped `hooks.json` with `SessionStart`→💬, `UserPromptSubmit`→🤖, and `Stop`→💬. But codex-cli 0.130.0's `HookEventNameWire` schema (extracted from the binary) has **no `Stop`/turn-end event**: `PreToolUse`, `PermissionRequest`, `PostToolUse`, `PreCompact`, `PostCompact`, `SessionStart`, `UserPromptSubmit`. So on Codex the 🤖 marker set at `UserPromptSubmit` could never return to 💬 — a Codex worktree stuck at "working" for the entire session. That's worse than the docs claimed ("rests at 💬"). This removes the marker hooks entirely until Codex exposes a turn-end hook event. The Codex plugin now ships only the configuration skill; activity tracking joins worktree-isolation and `/wt-switch-create` as Claude-Code-only. A re-enablement comment (exact conditions + the files to restore) lives in `src/commands/config/codex.rs` and a new `CLAUDE.md` → "Codex Plugin" section, which also documents the accepted tradeoff that the `skills/` symlink exposes the Claude-only `wt-switch-create` skill to Codex (harmless — Codex can't act on it). Bundled adjacent fixes: `plugin.json` metadata drift (`description`/`longDescription` no longer claim activity hooks; `homepage`/`websiteURL` `https://worktrunk.dev/claude-code/` → `https://worktrunk.dev`), and the install/uninstall/`--help`/docs text made honest about Codex having only the configuration skill. **Reviewer navigation:** `src/commands/config/codex.rs` + `src/cli/config.rs` (runtime + `--help` text), `docs/content/claude-code.md` (skill ref + `llms.txt` auto-synced), `CLAUDE.md` (new section), `tests/integration_tests/config_show.rs` (`test_codex_plugin_metadata_is_valid_json` now asserts `hooks` absent + guards metadata regression). Snapshot env-block churn is stale-fixture convergence to the repo's current `/nonexistent/wt/` convention, not a leak. **Testing:** Full pre-merge gate green locally (3704 tests, 0 skipped; lints clean). The metadata test was strengthened; the marker behavior itself is removed, so there's nothing runtime-testable to add. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
f00b787e1b |
fix(codex): relocate plugin into subdir so the marketplace resolves it (#2782)
## Problem #2780 put `.agents/plugins/marketplace.json` at the path codex-cli reads, but the plugin was still **uninstallable**. Driving the Codex `/plugins` TUI surfaced the real, complete set of constraints in codex-cli 0.130.0: 1. A plugin `source` must be the **object** form `{"source":"local","path":"./plugins/<name>"}` — a bare string `"./"` fails with `local plugin source path must not be empty`. 2. The path must be a **non-empty subdirectory**. Codex resolves it relative to the marketplace root (repo root), so a repo-root plugin (`./.codex-plugin/` at the top) can't be referenced at all — every Codex marketplace plugin lives in `./plugins/<name>/.codex-plugin/plugin.json`, exactly like the OpenAI-curated marketplace. So the plugin had to move into a subdirectory. ## Changes - Move `.codex-plugin/` → `plugins/worktrunk/.codex-plugin/` and `hooks/` → `plugins/worktrunk/hooks/` (a plugin-root sibling, matching the curated layout where `skills/` and `assets/` sit beside `.codex-plugin/`). - `plugins/worktrunk/skills` → symlink to `../../skills` so the shared `worktrunk` + `wt-switch-create` skills stay single-source (no duplication; the repo still auto-syncs `skills/worktrunk/reference/`). - Rewrite `.agents/plugins/marketplace.json` with the object `source`, `policy.installation: "AVAILABLE"`, and `category` that Codex actually parses. - `plugin.json` `hooks` → `./hooks/hooks.json` (plugin-root relative). - Update the install hint (`src/commands/config/codex.rs`), docs, auto-synced skill reference, the validity test, and the install snapshot. ## Verification Verified end-to-end against codex-cli 0.130.0: repointed a local marketplace at the worktree and drove `/plugins`. The `Worktrunk` marketplace now appears (plugin count 123 → 124), the plugin shows as **Available** with **both skills resolved** (`worktrunk:worktrunk`, `worktrunk:wt-switch-create` — the symlink works), and it installs. `test_codex_plugin_metadata_is_valid_json` now asserts the object `source`, `policy.installation`, `interface.displayName`, and the new paths. `test_docs_are_in_sync` and the install snapshot are updated; full `config_show` module (119 tests) + pre-commit pass. **Caveat (documented, not a regression):** plugin-bundled *command* hooks remain gated by Codex's `plugin_hooks` feature flag — already covered in the troubleshooting docs (`codex features enable plugin_hooks`). The pre-install plugin detail view shows "No plugin hooks", consistent with every one of the 123 curated plugins (none bundle command hooks, so this path has no working precedent to verify against in-TUI). Marker behavior with `plugin_hooks` enabled is the remaining thing to confirm in a real installed session. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |