Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,13 @@ Load relevant skills before starting; reload when scope changes mid-session. Pro

## Documentation

Behavior changes require doc updates. `src/cli/mod.rs` (`after_long_help` plus clap attributes) is the PRIMARY SOURCE for command pages; never hand-edit the generated mirrors under `docs/content/` or `skills/worktrunk/reference/`. Ask: "does `--help` still describe what the code does?" After any doc change run `cargo test --test integration test_docs_are_in_sync`; editing help text (`after_long_help`, `about`, arg docs) also changes the rendered `--help` snapshots, which that test leaves untouched — regenerate them with `cargo insta test --accept -- --test integration "test_help"` (the pre-merge hook runs both). Sync taxonomy, help-text authoring (three render contexts, link text, config-TOML blocks): `docs/CLAUDE.md`.
Behavior changes require doc updates. `src/cli/mod.rs` (`after_long_help` plus clap attributes) is the PRIMARY SOURCE for command pages; never hand-edit the generated mirrors under `docs/content/`, `skills/worktrunk/reference/`, or `plugins/worktrunk/skills/`. Ask: "does `--help` still describe what the code does?" After any doc change run `cargo test --test integration test_docs_are_in_sync`; editing help text (`after_long_help`, `about`, arg docs) also changes the rendered `--help` snapshots, which that test leaves untouched — regenerate them with `cargo insta test --accept -- --test integration "test_help"` (the pre-merge hook runs both). Sync taxonomy, help-text authoring (three render contexts, link text, config-TOML blocks): `docs/CLAUDE.md`.

Design proposals live in `design/` and are review-only: open a PR to discuss, then close it without merging. They guide implementation PRs but don't land on `main`.

## Plugin Layout

Per-tool layout and path resolution (Claude/Codex/Gemini), the convention-only Claude manifest, the Codex inline-hooks rationale, the accepted `wt-switch-create` tradeoff, and `test_plugin_layout_is_consolidated`: `plugins/worktrunk/CLAUDE.md`.
Per-tool layout and path resolution (Claude/Codex/Gemini), the convention-only Claude manifest, the Codex inline-hooks rationale, the generated plugin-skills mirror, the accepted `wt-switch-create` tradeoff, and `test_plugin_layout_is_consolidated`: `plugins/worktrunk/CLAUDE.md`.

## Data Safety

Expand Down
1 change: 0 additions & 1 deletion plugins/worktrunk/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@
]
}
},
"skills": "./skills/",
"interface": {
"displayName": "Worktrunk",
"shortDescription": "Git worktree workflows for parallel Codex sessions.",
Expand Down
70 changes: 54 additions & 16 deletions plugins/worktrunk/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ worktrunk/ ← repo root = marketplace root
├── .agents/plugins/marketplace.json← Codex pointer (source → ./plugins/worktrunk)
├── gemini-extension.json ← Gemini manifest (extensionPath = repo root)
├── hooks/hooks.json ← Gemini activity hooks (call the wt.sh below)
├── skills -> (this dir) Gemini reads ${extensionPath}/skills = repo-root skills/
├── skills/ ← real dir; Gemini reads ${extensionPath}/skills directly
└── plugins/worktrunk/ ← plugin root (Claude + Codex resolve source here)
├── .claude-plugin/plugin.json ← Claude manifest (metadata only — NO `hooks`
│ or `skills` keys; components load by
Expand All @@ -28,8 +28,10 @@ worktrunk/ ← repo root = marketplace root
│ $CLAUDE_PLUGIN_ROOT, Codex via $PLUGIN_ROOT,
│ Gemini via
│ ${extensionPath}/plugins/worktrunk/hooks/wt.sh
├── skills -> ../../skills ← symlink; single-sources skills across all
│ tools and the docs auto-sync
├── skills/ ← generated real-file mirror of repo-root
│ skills/ (test_docs_are_in_sync; never
│ hand-edit) — real files because Codex's
│ installer drops symlinks, see below
├── CLAUDE.md / README.md
└── (Codex activity hooks live *inline* in .codex-plugin/plugin.json's
`hooks` key — see Known Limitations below)
Expand All @@ -53,25 +55,61 @@ Path resolution differs by tool, all verified end-to-end against the real CLIs:
conventional path is dead config that can only mask a mislocated file.
- Skills are auto-discovered by scanning `skills/` for `<dir>/SKILL.md`; a
`skills` manifest array only *adds* directories to that scan, so listing
the defaults is redundant. The installer dereferences the `skills` symlink
into a real directory in the install cache.
the defaults is redundant.

`$CLAUDE_PLUGIN_ROOT` is the plugin root.
- **Codex**: `.agents/plugins/marketplace.json` `source` object
`{ "source": "local", "path": "./plugins/worktrunk" }`. Codex reads
`plugins/worktrunk/.codex-plugin/plugin.json`. `skills: "./skills/"` resolves
through the same symlink.
- **Codex** (codex-cli 0.144.1): `.agents/plugins/marketplace.json` `source`
object `{ "source": "local", "path": "./plugins/worktrunk" }`. Codex reads
`plugins/worktrunk/.codex-plugin/plugin.json`. Skills load by convention —
with no `skills` manifest key, Codex scans `<plugin-root>/skills/`; an
explicit `"skills": "./skills/"` names the same directory, so the manifest
carries no `skills` key. The scanned tree is the real-file mirror ("Plugin
skills are a generated mirror" below).
- **Gemini**: `gemini-extension.json` at the repo root; `${extensionPath}` is
the repo root, so `${extensionPath}/skills/` is the repo-root `skills/`
directly and `hooks/hooks.json` (repo root) calls the canonical shim at
`${extensionPath}/plugins/worktrunk/hooks/wt.sh`. No symlink or copy.

All three tools pick up the whole `skills/` directory (Claude and Codex through
the symlink, Gemini directly), so a new repo-root skill ships everywhere with
no manifest change — provided its directory contains a `SKILL.md`
(`test_plugin_layout_is_consolidated` enforces that; a directory without one is
silently ignored). Claude-only skills reach the other tools too (accepted
tradeoff — see Known Limitations below).
All three tools pick up the whole `skills/` set — Gemini reads the repo-root
directory, Claude and Codex ship the plugin mirror — so a new repo-root skill
ships everywhere once `test_docs_are_in_sync` regenerates the mirror, provided
its directory contains a `SKILL.md` (`test_plugin_layout_is_consolidated`
enforces that; a directory without one is silently ignored). Claude-only
skills reach the other tools too (accepted tradeoff — see Known Limitations
below).

### Plugin skills are a generated mirror

`plugins/worktrunk/skills/` is a real-file mirror of the authored repo-root
`skills/`, regenerated — symlinks dereferenced, stale files deleted — by the
`sync_plugin_skills_mirror` stage of `test_docs_are_in_sync`; never hand-edit
it. Repo-root `skills/` stays the authored home: Gemini reads it directly and
the docs sync writes into it.

The mirror holds real files because of how the plugin ships, verified
end-to-end against codex-cli 0.144.1 (scratch marketplaces through
`codex plugin marketplace add` + `codex plugin add`, skill inventory read with
`codex debug prompt-input`):

- `codex plugin add` copies the plugin into
`$CODEX_HOME/plugins/cache/<marketplace>/<plugin>/<version>` with a copier
that handles only regular files and directories, silently skipping symlink
entries (`copy_dir_recursive` in `codex-rs/core-plugins/src/store.rs`), and
sessions load from that cache copy. A symlink anywhere in the tree — a
top-level `skills` link or a nested one like `reference/README.md` — ships
no content. No manifest value can bridge it: manifest paths must stay within
the plugin root (`..` and absolute paths are rejected,
`resolve_manifest_path` in `codex-rs/core-plugins/src/manifest.rs`).
- Codex's convention scan (`default_skill_roots`, the empty-`skills` branch of
`plugin_skill_roots` in `codex-rs/core-plugins/src/loader.rs`) reads the
mirror like any directory.
- Claude's installer dereferences symlinks, so a symlinked `skills/` worked
for Claude; the mirror serves it identically. A symlink also materializes as
a plain text file on Windows checkouts, which shipped no skills from a
Windows clone to Claude or Codex.

`test_plugin_layout_is_consolidated` pins the no-symlinks invariant;
`test_docs_are_in_sync` pins content equality with repo-root `skills/`.

## Known Limitations

Expand Down Expand Up @@ -105,4 +143,4 @@ The events (Codex's `HookEventsToml` vocabulary, verified against `codex-rs/conf

### Accepted tradeoff: shared `skills/` exposes `wt-switch-create`

Codex's `"skills": "./skills/"` and Gemini's `${extensionPath}/skills/` both resolve the entire repo-root `skills/`, including `wt-switch-create`, which depends on Claude session-cwd switching (`EnterWorktree`) that neither provides. Accepted: a tool loading a skill it can't act on is harmless, and a single repo-root `skills/` keeps the `worktrunk` skill single-source across all three tools and the docs sync. Don't add per-tool skills subtrees to exclude it.
Codex's mirrored `skills/` and Gemini's `${extensionPath}/skills/` both carry the entire skill set, including `wt-switch-create`, which depends on Claude session-cwd switching (`EnterWorktree`) that neither provides. Accepted: a tool loading a skill it can't act on is harmless, and a single authored `skills/` keeps the `worktrunk` skill single-source across all three tools and the docs sync. Don't add per-tool skills subtrees to exclude it.
1 change: 0 additions & 1 deletion plugins/worktrunk/skills

This file was deleted.

Loading
Loading