mirror of
https://github.com/max-sixty/worktrunk.git
synced 2026-09-14 20:00:38 +08:00
codex/remove-codex-cloud-specific-tests
16 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
bb580ed46b |
fix(plugin): WorktreeRemove hook skips a path holding no worktree (#3767)
## Problem The `WorktreeRemove` hook guards with `[ -e "$p" ]` — #3493's narrowing, which made the hook a no-op when the recorded worktree path is gone. #3753 reports the third state between "gone" and "live": a path that **exists but holds no worktree**. A skeleton directory left by an interrupted create or remove has no `.git`, is invisible to both `git worktree list` and `wt list`, and passes the existence guard, so `wt remove <path>` runs and resolves the path as a *branch* name: ``` ✗ No branch named /…/.claude/worktrees/<name> ↳ To list branches, run wt list --branches --remotes # exit 1 ``` An out-of-tree skeleton gives `fatal: not a git repository` instead. Claude Code reads a nonzero `WorktreeRemove` as a failed removal and keeps the session row, so the finished background session can never be deleted with Ctrl+X — the same end symptom as #3488, but permanent: prune ignores a directory that was never a worktree, so nothing heals it. ## Solution Test for git's marker rather than for the directory: ```diff - [ -e "$p" ] || exit 0 + [ -e "$p/.git" ] || exit 0 ``` Every linked worktree carries a `.git` file, and a dirty, locked, or unmerged one still does, so genuine failures keep surfacing loudly — the #2939 spirit the #3493 guard was written to preserve. Leniency stays scoped to "no worktree lives here" rather than becoming a blanket success, and the change stays inside the single-quoted `bash -c` body, so outer login-shell (fish/zsh/bash) parsing is untouched. `-e` rather than `-f` is deliberate: the *main* worktree's `.git` is a directory, so `-e` keeps `✗ The main worktree cannot be removed` loud where `-f` would silently no-op it. This composes with #3754 rather than overlapping it: the guard now runs before `-C "$p"`, so `-C` only ever resolves a path that really is a worktree. One state changes beyond the reported one, in the same direction: a registered worktree whose `.git` file was deleted by hand already failed the hook (`fatal: not a git repository`), and now exits 0, leaving a prunable registration for `git worktree prune`. Nothing `wt remove` could previously remove is skipped. ## Testing `test_worktree_remove_hook_skips_path_holding_no_worktree` runs the real command out of `hooks.json` under `bash`, feeding it the recorded path on stdin exactly as Claude Code does. It pins three directions, so neither a blanket `exit 0` nor a swallowed `wt remove` failure can satisfy it: a skeleton is a no-op and is left on disk (both in-tree and out-of-tree), a dirty worktree still fails with `uncommitted changes` and stays put, and that same worktree once clean is still removed. <details><summary>Mutation evidence and hand-verified states</summary> Each mutation was applied to `hooks.json` and the test re-run: | mutation | result | |---|---| | `[ -e "$p" ]` (the pre-fix guard) | fails — `hook must be a no-op for a path holding no worktree` | | whole body replaced with `exit 0` | fails — `hook must still refuse a dirty worktree, and for that reason` | | `wt remove … \|\| exit 0` (failure swallowed) | fails — same assertion | Hand-verified against the built binary via `WORKTRUNK_BIN`, firing the hook as Claude Code does: | state | before | after | |---|---|---| | skeleton dir, in-tree | exit 1, `No branch named …` | exit 0, directory untouched | | skeleton dir, out-of-tree | exit 1, `fatal: not a git repository` | exit 0, directory untouched | | recorded path gone | exit 0 | exit 0 | | clean worktree | removed | removed | | dirty worktree | exit 1, `has uncommitted changes` | unchanged | | unmerged branch | worktree removed, branch retained + `-D` hint | unchanged | | main worktree | exit 1, `The main worktree cannot be removed` | unchanged | | registered worktree, `.git` deleted by hand | exit 1, `fatal: not a git repository` | exit 0, prunable entry left | The test is gated on `all(unix, feature = "shell-integration-tests")`, which the required `test` jobs and the coverage run both enable; the hook parses its stdin with `jq`. **Not verified:** Claude Code's own session-row teardown — CI can't drive the agent UI. The evidence here is the hook's exit status, which is what Claude Code branches on per #3488/#3493. </details> The full pre-merge gate passes locally (4571 tests, clippy, doctests, docs sync, no pending snapshots). ## Relation to #3755 Supersedes worktrunk-bot's #3755, whose one-line hook edit is byte-identical to this one. The difference is test coverage: #3755's test passes against a hook with `|| exit 0` appended to `wt remove`, which would silently discard the dirty-worktree refusal — verified by running that test file against the mutation. This one also covers the out-of-tree skeleton. #3755's `flake.nix` addition of `jq` is left out: the devShell already omits nushell so it can't run the shell-integration suite regardless, and the nix test derivation runs default features only, where this test isn't compiled. Closes #3753. Thanks to @judewang for the report, the reproduction, and the fix direction — including the note that `git -C "$p" rev-parse --is-inside-work-tree` is not a usable test, since discovery walks up to the parent for a nested skeleton. > _This was written by Claude Code on behalf of max-sixty_ Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
3817df0795 | fix(plugin): resolve WorktreeRemove against the worktree path, not the project dir (#3754) | ||
|
|
dbefb98ed4 |
fix(plugin): add pipefail to WorktreeCreate hook so wt failures surface (#3546)
## Problem
The Claude plugin's `WorktreeCreate` hook
(`plugins/worktrunk/hooks/hooks.json`) pipes the `wt` result straight
into `jq`:
```
bash -c 'name=$(jq -er .name) || exit 1; cd "${CLAUDE_PROJECT_DIR:-.}" || exit 1; bash "$CLAUDE_PLUGIN_ROOT/hooks/wt.sh" switch --create "$name" --no-cd --format=json | jq -er .path'
```
Without `set -o pipefail`, the pipeline's exit status is `jq`'s, not
`wt`'s. On jq 1.6, `jq -er .path` exits **0** on the empty stdout of a
failed `wt`:
```console
$ printf '' | jq -er .path; echo $? # jq 1.6
0
```
So when `wt switch --create` dies (e.g. an existing-branch collision
after the branch/worktree were already created by an earlier partial
run), the hook "succeeds" while printing nothing. Claude Code then
reports the misleading `WorktreeCreate hook failed: hook succeeded but
returned no worktree path` instead of `wt`'s real stderr — as reported
in #3545.
The project's own `skills/wt-switch-create/rationale.md` already
documents the `bash -c 'set -o pipefail; …'` wrapper as the intended
design ("The hooks.json pipefail wrapper"), but the command in
`hooks.json` never actually carried it. This PR brings the code in line
with its own documentation.
## Solution
Prefix the existing `bash -c` command with `set -o pipefail;`. The
wrapper is already `bash -c`, so this is safe — dash rejects `set -o
pipefail` fatally, which is precisely why the `bash -c` wrapper (rather
than the login shell) exists per the rationale. With pipefail, the
pipeline surfaces `wt`'s nonzero exit regardless of jq version.
## Testing
Added a reproduction assertion to `test_plugin_layout_is_consolidated`
(`tests/integration_tests/config_show.rs`) that fails when the
`WorktreeCreate` hook command lacks `set -o pipefail` — it fails on the
pre-fix `hooks.json` and passes after. Also verified the pipeline shape
directly:
```console
$ bash -c 'set -o pipefail; (echo "✗ Branch foo already exists" >&2; exit 1) | jq -er .path'; echo $?
✗ Branch foo already exists
4 # nonzero -> hook correctly fails, wt's stderr shown
$ bash -c 'set -o pipefail; echo "{\"path\":\"/tmp/wt/foo\"}" | jq -er .path'; echo $?
/tmp/wt/foo
0 # success path unchanged
```
The `bash -c '…'` body stays single-quoted, so
`test_claude_hook_commands_parse_in_all_shells` (login-shell parse
check) is unaffected — the outer shells never look inside the quotes.
---
Closes #3545 — automated triage
Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.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) | ||
|
|
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>
|
||
|
|
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) | ||
|
|
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) | ||
|
|
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> |