Commit Graph

6 Commits

Author SHA1 Message Date
Worktrunk Bot 80e795b271 fix(plugin): run the Codex Windows hooks through Git Bash, not the WSL launcher (#4008)
## Problem

Codex resolves a hook `command` through the platform shell — `/bin/sh
-lc` on Unix, `cmd.exe /C` on Windows
([`default_shell_command`](https://github.com/openai/codex/blob/main/codex-rs/hooks/src/engine/command_runner.rs)).
All four Codex hooks in the plugin manifest lead with a bare `bash`, and
under `cmd.exe` that resolves through the Windows PATH to
`System32\bash.exe` — the WSL launcher, not Git Bash. In a sandboxed
session the launcher refuses to start (`Access is denied. Error code:
Bash/Service/CreateInstance/E_ACCESSDENIED`), so every prompt,
permission request, turn end, and session end raises a `Hook failed`
banner (#4007).

## Solution

Git for Windows stays a requirement; only the bare *name* `bash` is the
problem, since `cmd.exe` resolves it through PATH. Each hook now also
carries Codex's per-handler `commandWindows`, which **replaces**
`command` on Windows (`command_windows.unwrap_or(command)` in
`codex-rs/hooks/src/engine/discovery.rs`). It calls a new `cmd.exe`
shim, `plugins/worktrunk/hooks/wt.cmd`, that locates `bash.exe` by path
the way
[`find_git_bash`](https://github.com/max-sixty/worktrunk/blob/2405b8b434cfa9d7a604405d3ace966ada60ff8a/src/shell_exec.rs#L481-L518)
does in `src/shell_exec.rs` — derived from `git.exe`'s install
directory, then the system-wide and per-user install defaults — and then
runs the same `hooks/wt.sh` that Claude, Gemini, and Unix Codex already
go through. Worktrunk's own binary resolution stays in that one script
instead of being spelled a second time in cmd.

Two details the shim inherits from the Rust resolver: `Git\bin\bash.exe`
before `Git\usr\bin\bash.exe`, because the former is the wrapper that
sets up the MSYS environment for a caller outside Git Bash (which is
what puts `uname` within reach of `wt.sh`); and a PATH-scoped lookup,
because an unscoped `where` searches the current directory first — for a
hook that's the user's project, so a `git.exe` committed to a repo would
otherwise choose the bash every event runs. The lookup is spelled
`"%SystemRoot%\System32\where.exe" "$PATH:git.exe"` on both counts: the
`$PATH:` prefix scopes what is searched, and the absolute path to
`where.exe` closes the same surface one level up — `where` is
`System32\where.exe` rather than a cmd built-in, so cmd resolves that
bare name from the current directory too.

`wt.sh` now clears `WT` before its branches. On Windows all of them can
be skipped (neither `git-wt.exe` nor `wt` on PATH), and a hook is handed
the caller's whole environment, so an inherited `WT` was what the final
`command -v "$WT"` check accepted and ran.

The Windows commands brace the plugin root as `${PLUGIN_ROOT}` because
Codex substitutes only that form textually, before the shell runs; the
unbraced `$PLUGIN_ROOT` the Unix commands use survives to `/bin/sh`, and
`cmd.exe` would pass it through literally. The tail is `|| exit /b 0`,
the cmd.exe spelling of the Unix `|| true`: a marker is decoration, and
a nonzero exit is what raises the banner. `.gitattributes` pins
`plugins/worktrunk/hooks/*.cmd` to a CRLF checkout, since cmd.exe
resolves a `goto` label by seeking through the file and can fail that
search on an LF-only batch file.

Two doc changes ride along. `plugins/worktrunk/CLAUDE.md` records why
every hook carries `commandWindows`, the shim's resolution order and the
two lookup-scoping decisions, and why `SessionEnd` keeps `timeout: 3`
(Codex's ceiling for that event, so the longer Windows chain has no more
budget to ask for). And because the CHANGELOG entry opens an `##
Unreleased` section, `.claude/skills/release/SKILL.md` step 9 now says
to rename that heading at release time rather than insert a new one
above it — otherwise the release ships a stale `## Unreleased`.

## Testing

Six tests, all new:

- `test_codex_hooks_carry_windows_commands` (all platforms) — pins that
every Codex command hook has a `commandWindows`, that it names neither
`bash` nor bare `wt`, that it calls the shim, and that the two
`PLUGIN_ROOT` spellings stay on their respective sides. Written first:
it failed on the manifest as shipped, with the exact hook command from
the report.
- `test_codex_windows_hook_commands_set_the_marker` (Windows leg of CI)
— runs the real `commandWindows` the way Codex spawns it, reproducing
both steps: the `${PLUGIN_ROOT}` substitution and `cmd.exe /C
"<command>"` with the command line as a single quoted raw argument. It
asserts `UserPromptSubmit` stores 🤖, `Stop` replaces it with 💬,
`SessionEnd` clears it, and that a hook which cannot find worktrunk
still exits 0. PATH is pinned to the shape a default Git for Windows
install produces — `Git\cmd` and nothing else from the install — so the
run covers cmd.exe's quote handling, the shim's search for bash, `wt.sh`
running under the bash it picks, and the emoji surviving both hops.
- `test_wt_sh_ignores_an_inherited_wt` (Windows leg) — points `WT` at a
real worktrunk, spelled the way bash can run it, on a PATH where `wt.sh`
finds none itself, and pins that the inherited value is not what runs.
- `test_shim_ignores_a_git_in_the_current_directory` (Windows leg) —
plants an unrunnable `git.exe` in the hook's current directory plus the
`bash.exe` the shim would derive from it, puts a real Git and worktrunk
on the pinned PATH, and asserts the shim prints a version line — which
it could not do had it taken the decoy.
- `test_shim_ignores_a_where_in_the_current_directory` (Windows leg) —
the same question one level up: a `where.bat` in the hook's current
directory names a decoy Git install whose `bin\bash.exe` exists, so an
unscoped `where` would reach the derive branch and leave `BASH` set and
unrunnable rather than falling through to a real install. A separate
probe pins the premise — that the planted `where` really does shadow
`System32\where.exe` — so the test cannot go green because the decoy was
never consulted.
- `test_shim_derives_bash_from_the_git_on_path` (Windows leg) — points
`ProgramFiles` and `LOCALAPPDATA` at an empty directory, which leaves
the derive branch as the only route to a bash. The two tests above go
red only when the shim picks the *wrong* bash; this one goes red when
the lookup finds nothing, so it is what observes that
`"%SystemRoot%\System32\where.exe" "$PATH:git.exe"` survives the quoting
`for /f` wraps it in.

<details><summary>What this does not verify</summary>

Nothing here drives a real Codex session on Windows, so the end-to-end
claim — that Codex selects `commandWindows` and that the banner stops —
rests on reading
`codex-rs/hooks/src/engine/{discovery,command_runner}.rs` rather than on
observation. What CI does exercise is the command string itself,
executed the way that source says Codex executes it.

The shim's two standard-install fallbacks (`%ProgramFiles%\Git`,
`%LOCALAPPDATA%\Programs\Git`) are unexercised by CI. Every test that
reaches a bash takes the derive branch above them, and the one test that
touches the fallbacks empties them to force that branch rather than to
exercise them; they share the whole tail with it.

Two adjacent things are deliberately left alone, as separate concerns:
the Gemini hooks at the repo-root `hooks/hooks.json` use the same bare
`bash` (Gemini's Windows hook execution isn't established here), and
Claude's `hooks/hooks.json` is unaffected because Claude Code runs hook
commands through Git Bash on Windows.

</details>

---
Closes #4007 — automated triage

---------

Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
2026-09-06 16:26:17 -07:00
Maximilian Roos cfd6f099bc fix(codex): clear activity marker on session end (#3660)
Codex now exposes `SessionEnd`, but Worktrunk’s Codex plugin only
returned the activity marker to idle at turn end. This adds a
main-session exit hook that clears the marker, using Codex’s
three-second maximum hook timeout so cleanup has the best chance to
complete without delaying shutdown.

The CLI help, plugin-layout guidance, user documentation, generated
mirrors, metadata test, and help snapshot now describe and verify the
complete Codex lifecycle.

Tested with `cargo run -- hook pre-merge --yes` (4,564 tests passed; 1
skipped).

> _This was written by Claude Code on behalf of max_.
2026-07-29 20:57:26 -07:00
Maximilian Roos 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>
2026-07-12 17:48:30 -07:00
Worktrunk Bot 512a94de2f feat(plugin): ship Codex-native activity hooks (#3364) 2026-07-06 23:05:15 -07:00
Maximilian Roos 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>
2026-05-17 15:29:54 -07:00
Maximilian Roos 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>
2026-05-17 14:29:03 -07:00