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
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **Antigravity CLI sessions report to lich.** A sixth harness, and the first
whose packaging the plugin has to satisfy as well as its events: an Antigravity
plugin is a directory holding a `plugin.json`, whose `hooks.json` and `skills/`
are loaded from beside it. Both names are fixed, so those two files are the
ones in this repository that live at the root — `agy plugin validate .` passes
on the clone, and linking it into `plugins/` installs the hooks and the `theme`
skill in one step.

It reports the session id and `busy` on `PreInvocation`, the running tool on
`PreToolUse`, the git-status refresh on `PostToolUse`, and `done` plus the
title on `Stop` — the title being the transcript's first `USER_INPUT` line,
unwrapped from its `<USER_REQUEST>` tags. `waiting` and `idle` are not
reported: no event has been measured for either, and `Stop` ends one execution
loop rather than the conversation.

Two Antigravity rules the shared scripts had to learn. Its hooks are run
through `sh -c` with the working directory set to the plugin root and no
plugin-root variable of any kind, so the registration spells relative paths.
And a hook answers on stdout, where the answer is acted on — `decision` is
required before a tool call — so the registration appends the verdict each
event asks for, leaving the scripts silent for Codex, which reads their stdout
as an answer to a permission request.

The provider id is registered in lich (omartelo/lich#370), so the fixtures
enumerate `antigravity` like every other provider and the suite holds this
client to it.

- **A waiting card says what the session is waiting for.** The `waiting` report
now carries a `reason`, so the bell says *why* rather than only *that*. Each
harness hands over something different to say it with: Claude Code's
Expand Down
25 changes: 23 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# lich-plugin

The agent side of the integration with **lich** — a harness that orchestrates agent CLI sessions. This companion plugin is how lich observes and acts inside a running session. It ships hooks — each following a contract documented in `docs/` — plus skills for work that targets lich itself. One plugin, packaged for **Claude Code**, **OpenAI Codex**, **opencode**, **omp** (oh-my-pi) and **Crush** from the same repository root.
The agent side of the integration with **lich** — a harness that orchestrates agent CLI sessions. This companion plugin is how lich observes and acts inside a running session. It ships hooks — each following a contract documented in `docs/` — plus skills for work that targets lich itself. One plugin, packaged for **Claude Code**, **OpenAI Codex**, **Antigravity CLI**, **opencode**, **omp** (oh-my-pi) and **Crush** from the same repository root.

## Structure

Expand All @@ -9,10 +9,12 @@ The agent side of the integration with **lich** — a harness that orchestrates
.claude-plugin/marketplace.json # marketplace, Claude Code
.codex-plugin/plugin.json # plugin manifest, Codex
.agents/plugins/marketplace.json # marketplace, Codex
plugin.json # plugin manifest, Antigravity — name and place both fixed
hooks.json # hook registration, Antigravity — likewise
hooks/hooks.json # hook registration, Claude Code
hooks/codex-hooks.json # hook registration, Codex
hooks/crush-hooks.json # hook registration, Crush (hand-merged into crush.json)
hooks/ # hook scripts ($CLAUDE_PLUGIN_ROOT/hooks/<script>, all three)
hooks/ # hook scripts (all four run them; only how they are addressed differs)
hooks/detail.jq # what a tool call acts on, shared by two of those scripts
hooks/win-run.cmd # runs one of those scripts on Windows (Codex)
opencode/lich.js # opencode client: a module, not a command
Expand All @@ -22,6 +24,17 @@ skills/ # skills (skills/<name>/SKILL.md, all)
tests/ # hook payloads asserted against lich's fixtures
```

**Antigravity** runs the scripts, but on its own terms, and both of them are in
the file layout above. Its plugins are directories — `plugins/<name>/plugin.json`
with `hooks.json` and `skills/` beside it — and neither filename is
configurable, which is why those two sit at the repository root instead of in a
namespaced folder: the root is the plugin directory once the clone is linked in.
It also sets no plugin-root variable, running a hook through `sh -c` with the
working directory set to that root, so its registration alone spells relative
paths; and it reads a JSON verdict off the hook's stdout, so the registration
appends one per event while the scripts stay silent. `agy plugin validate .`
checks the bundle from a clone.

Three harnesses do not fit the script model, each its own way. **opencode** runs
no commands: a plugin there is a JavaScript module its server imports, so all
four reports live in `opencode/lich.js`. **omp** has a `--hook` flag but merges
Expand Down Expand Up @@ -88,6 +101,9 @@ the network; CI diffs the copies against upstream and fails on drift.
- One script per contract, shared by every provider that runs commands. Registration differs per harness, the report does not — extend a script to read a second transcript format before adding a second script. A harness that runs no commands at all is the one case for a client of its own, which is what `opencode/lich.js` and `omp/lich.js` are; they still send the same payloads to the same endpoints.
- opencode and omp both await their plugin handlers, so neither `lich.js` ever awaits a report or throws — that is their version of "always exit 0". The omp one also subscribes to nothing outside lich: omp loads it on every run on the machine, so the gate has to be in the module.
- **A module client names only events it was measured emitting.** A name read off a type declaration is a report that silently never fires, which is worse than an absent feature — the omp `waiting` state is left unreported for exactly this reason.
- **The same goes for every other name a registration carries** — an event, a tool a matcher names, a payload field, a plugin-root variable. All of them fail the same way: quietly, on every event, with a green test suite, because a harness that never runs the hook reports nothing to disagree with.
- **A name is measured off a real payload, or it is a guess.** Not from a harness you already support, and not from a symbol table or an enum either — those are the *implementation's* names, and something may map them before they reach a hook. Antigravity is the worked example in both directions: its guide says a tool name is the step type lowercased with `CORTEX_STEP_TYPE_` dropped, and the enum's `MCP_TOOL` still arrives as `call_mcp_tool`. Its write tools really are `write_to_file` and `replace_file_content`, which the enum does not contain. Run the turn, print the payload, read the name off it — a matcher built any other way matches nothing, and losing a report costs no error, only the report.
- **A test that supplies what the harness supplies proves nothing.** The suite exports `$CLAUDE_PLUGIN_ROOT` because Claude Code and Codex do; running Antigravity's registration under it hid the fact that Antigravity does not. Each harness's registration is run the way *that* harness runs it — see `pluginRoot` in `tests/hooks.test.mjs`.

## Local testing

Expand All @@ -97,6 +113,11 @@ claude --plugin-dir .

Codex has no equivalent one-shot flag; install the clone as a local marketplace instead (`codex plugin marketplace add .` then `codex plugin add lich@lich-plugin`), and trust the hooks once with `/hooks`.

Antigravity has none either, but it does have a checker: `agy plugin validate .`
reads the clone as a plugin and says what it loaded (`skills: 1`, `hooks: 1`) —
a missing or misplaced `plugin.json` fails it outright. To run it for real,
symlink the clone into `~/.gemini/config/plugins/lich`.

## Release

Same standard as the `lich` repository (Keep a Changelog + SemVer). Releases are cut by tagging `vX.Y.Z`:
Expand Down
27 changes: 21 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# lich-plugin

The agent side of the [lich](https://github.com/omartelo/lich) integration. lich is a harness that orchestrates agent CLI sessions; this companion plugin gives it eyes and hands inside each session. It installs on **Claude Code**, **OpenAI Codex**, **opencode**, **omp** (oh-my-pi) and **Crush** from this same repository. Every hook implements a contract that is canonical in the lich repository (`docs/hooks/` there); the docs in `docs/` here point at each contract and describe the client side:
The agent side of the [lich](https://github.com/omartelo/lich) integration. lich is a harness that orchestrates agent CLI sessions; this companion plugin gives it eyes and hands inside each session. It installs on **Claude Code**, **OpenAI Codex**, **Antigravity CLI**, **opencode**, **omp** (oh-my-pi) and **Crush** from this same repository. Every hook implements a contract that is canonical in the lich repository (`docs/hooks/` there); the docs in `docs/` here point at each contract and describe the client side:

- **session state** — `busy`/`done` on the session card (`UserPromptSubmit`/`Stop`)
- **session start** — persists the agent's session id on the lich session (`SessionStart`)
- **session state** — `busy`/`done` on the session card (`UserPromptSubmit`/`PreInvocation`/`Stop`)
- **session start** — persists the agent's session id on the lich session (`SessionStart`/`PreInvocation`)
- **session title** — names the card after the session's own title (`Stop`)
- **session touched** — refreshes the card's git status right after file-mutating tools (`PostToolUse`)

The four are what Claude Code, Codex, opencode and omp report. **Crush reports two of them** — its session id and the git-status refresh — because `PreToolUse` is the only event it has, and a state nothing can end is worse on a card than no state. **omp reports all four minus the bell**: no event of its own for "your turn" has been measured yet, so its card shows a spinner while the agent waits on you. [docs/providers.md](docs/providers.md) has the event mapping per harness.
The four are what Claude Code, Codex, Antigravity, opencode and omp report. **Crush reports two of them** — its session id and the git-status refresh — because `PreToolUse` is the only event it has, and a state nothing can end is worse on a card than no state. **omp reports all four minus the bell**: no event of its own for "your turn" has been measured yet, so its card shows a spinner while the agent waits on you. [docs/providers.md](docs/providers.md) has the event mapping per harness.

On **opencode** it also carries the other direction: the seven operations lich
offers a session for driving the sessions beside it — list, send, wait, reply,
Expand All @@ -19,7 +19,7 @@ two cases where they are deliberately absent.

It also ships skills for the parts of lich you configure from inside a session:

- **theme** (`/lich:theme` in Claude Code; on Codex the `theme` skill loads from its description) — write, port or fix a lich color theme: the app tokens, the xterm palette, where the file goes, and a validator for the rules that otherwise fail silently
- **theme** (`/lich:theme` in Claude Code; on Codex and Antigravity the `theme` skill loads from its description) — write, port or fix a lich color theme: the app tokens, the xterm palette, where the file goes, and a validator for the rules that otherwise fail silently

## Structure

Expand All @@ -28,6 +28,8 @@ It also ships skills for the parts of lich you configure from inside a session:
.claude-plugin/marketplace.json # marketplace, Claude Code
.codex-plugin/plugin.json # plugin manifest, Codex
.agents/plugins/marketplace.json # marketplace, Codex
plugin.json # plugin manifest, Antigravity (name fixed, must sit at the root)
hooks.json # hook registration, Antigravity (likewise)
hooks/hooks.json # hook registration, Claude Code
hooks/codex-hooks.json # hook registration, Codex
hooks/crush-hooks.json # hook registration, Crush (merged into crush.json by hand)
Expand All @@ -43,7 +45,7 @@ skills/theme/ # theme skill: SKILL.md, template.json, valida
tests/ # hook payloads asserted against lich's fixtures
```

One set of hook scripts serves Claude Code, Codex and Crush — they live in `hooks/` and are referenced via `$CLAUDE_PLUGIN_ROOT/hooks/<script>`, a variable Codex sets too. opencode and omp run no commands: in both, what gets loaded is a JavaScript module, so each has a single-file client — `opencode/lich.js` and `omp/lich.js` — sending the same payloads to the same endpoints. [docs/providers.md](docs/providers.md) maps the layout and every harness's event names.
One set of hook scripts serves Claude Code, Codex, Antigravity and Crush — they live in `hooks/`. Claude Code and Codex reach them through `$CLAUDE_PLUGIN_ROOT/hooks/<script>`, a variable Codex sets too; Crush ships a placeholder the user replaces. Antigravity sets no such variable — it runs a hook with the working directory set to the plugin root, so its registration spells `hooks/<script>` relative to that, and it is the reason `plugin.json` and `hooks.json` sit at the root: Antigravity fixes both names and looks for them nowhere else. opencode and omp run no commands: in both, what gets loaded is a JavaScript module, so each has a single-file client — `opencode/lich.js` and `omp/lich.js` — sending the same payloads to the same endpoints. [docs/providers.md](docs/providers.md) maps the layout and every harness's event names.

## Installation

Expand All @@ -67,6 +69,19 @@ codex plugin add lich@lich-plugin

Then start a new session and run `/hooks` to review and trust the plugin's hooks — Codex does not run a plugin's hooks until you do, so until then the plugin is installed but silent. Hooks themselves are stable and on by default in current Codex; on older versions set `[features] hooks = true` in `~/.codex/config.toml`.

### Antigravity CLI

A plugin is a directory holding a `plugin.json`, under a `plugins/` folder in a customization root. Symlink the clone into one — globally, or into a project's `.agents/` to share it with the team:

```bash
mkdir -p ~/.gemini/config/plugins
ln -s "$PWD" ~/.gemini/config/plugins/lich
```

That is the whole install: the hooks come from `hooks.json` beside the manifest, and the `theme` skill from `skills/`. `agy plugin validate .` checks the bundle from the clone before you link it, `agy plugin list` shows it afterwards, and `agy plugin disable lich` turns it off again.

Without the plugin, [`hooks.json`](hooks.json) can be merged into `~/.gemini/config/hooks.json` (global) or `.agents/hooks.json` (per project) — but a hook's working directory is the folder holding the `hooks.json` that declared it, so replace each `hooks/` prefix with the absolute path to this clone.

### opencode

opencode has no marketplace: a plugin is a file in its plugin directory, so dropping it there is the install.
Expand Down
Loading
Loading