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

## [Unreleased]

### Added

- **omp (oh-my-pi) sessions report to lich.** A fifth harness, and the second
that loads a module rather than running commands: omp merges `--hook` and
`--extension` into one list and `import()`s every entry, so even the files it
calls hooks are modules. `omp/lich.js` is the whole client — the session id off
`ctx.sessionManager.getSessionId()` at `session_start`, `busy` on `input` and
`turn_start`, the tool line on `tool_call`, the git-status refresh on a
`tool_result` from `write`/`edit`/`bash`/`notebook`, and `done` plus the
generated title when a turn settles. Install is the file plus a line:
`~/.omp/agent/extensions/lich.js`, or any path named in `extensions:` in
`~/.omp/agent/config.yml`.

Two states are deliberately absent. Nothing reports `waiting`: omp has an
approval event, but it was never observed on a real run here, and a name taken
off a type declaration is a report that silently never fires — so an omp card
shows a spinner, not a bell, while the agent waits on you. Nothing reports
`idle` either, for the same reason no opencode session does: the process that
would send it is the one exiting.

The gate lives in the module rather than in a registration. omp discovers
`~/.claude` and `~/.omp` configuration wholesale, so a global install is loaded
by every omp run on the machine; with no lich environment the factory
subscribes to nothing at all.

Needs `omp` registered as a provider id in lich — until then the session-id
report is rejected and the card holds no id, while the other three land.

### Fixed

- **A Codex session on Windows reports again.** Every hook failed there —
Expand Down
23 changes: 16 additions & 7 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** 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**, **opencode**, **omp** (oh-my-pi) and **Crush** from the same repository root.

## Structure

Expand All @@ -15,14 +15,17 @@ hooks/crush-hooks.json # hook registration, Crush (hand-merged into c
hooks/ # hook scripts ($CLAUDE_PLUGIN_ROOT/hooks/<script>, all three)
hooks/win-run.cmd # runs one of those scripts on Windows (Codex)
opencode/lich.js # opencode client: a module, not a command
omp/lich.js # omp client: a module, not a command
docs/ # lich ⇄ plugin communication contracts
skills/ # skills (skills/<name>/SKILL.md, all)
tests/ # hook payloads asserted against lich's fixtures
```

Two 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`. **Crush** runs the scripts unchanged — its
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
it with `--extension` into one list it `import()`s, so its "hooks" are modules
too, and its client is `omp/lich.js`. **Crush** runs the scripts unchanged — its
hooks are Claude Code-compatible down to the stdin payload — but has exactly one
event, `PreToolUse`, so only the session id and the touched refresh are
registered for it.
Expand All @@ -35,6 +38,7 @@ Contracts are **canonical in the lich repository** (`docs/hooks/` there); this p
- [docs/session-start.md](docs/session-start.md) — Claude session id via `SessionStart`
- [docs/session-title.md](docs/session-title.md) — auto-generated `ai-title` via `Stop`
- [docs/session-touched.md](docs/session-touched.md) — git-status refresh signal via `PostToolUse` (file-mutating tools only)
- [docs/providers.md](docs/providers.md) — the per-harness map, including what installing on omp takes and what it cannot report

Each doc carries the event mapping for **every** provider, one column each — a contract is implemented once and registered per harness. [docs/providers.md](docs/providers.md) is the map: which file each harness reads, where the event vocabularies differ, and what adding another provider takes. A harness that cannot close a state does not get that report registered: an unendable `busy` is worse on a card than a card with no indicator.

Expand All @@ -58,8 +62,12 @@ environment → no report, exit 0 on a 500 or a refused connection).
opencode module, which is imported rather than spawned, and fed the event
payloads a real opencode run emits — including the sub-session ones it must
drop and the listener that never answers, which must not hold the turn.
[tests/omp.test.mjs](tests/omp.test.mjs) does the same for the omp module,
against the events and `ctx.sessionManager` methods measured on omp v17.3.0 —
pinned in a comment there, because 17.x moves fast — plus the rule that outside
lich it subscribes to nothing at all.
[tests/contract.mjs](tests/contract.mjs) is the fixtures, the assertions and the
stub, shared so both clients answer to the same lines. Node only, no
stub, shared so every client answers to the same lines. Node only, no
dependencies.

The fixtures are vendored in `tests/fixtures/` from lich
Expand All @@ -76,8 +84,9 @@ the network; CI diffs the copies against upstream and fails on drift.
- A hook must never block or fail the user's turn: short timeout, errors swallowed, always exit 0. On Codex the exit code is louder still — `2` on `PermissionRequest` denies the request, so silence and exit 0 are what keep a report an observation.
- Outside lich (env vars absent) every hook is a no-op with exit 0 — the plugin must be safe to install globally.
- A skill must be useful from any working directory: the user runs the agent CLI on their own project, not on the lich checkout.
- 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` is; it still sends the same payloads to the same endpoints.
- opencode awaits its plugin hooks, so `lich.js` never awaits a report and never throws — that is its version of "always exit 0".
- 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.

## Local testing

Expand Down
21 changes: 17 additions & 4 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** 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**, **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 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 and opencode 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. [docs/providers.md](docs/providers.md) has the event mapping per harness.
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.

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 Down Expand Up @@ -37,12 +37,13 @@ hooks/report-session-start.sh # session-start hook
hooks/report-title.sh # session-title hook
hooks/report-touched.sh # session-touched hook
opencode/lich.js # opencode client: all four reports plus the seven tools, one module
omp/lich.js # omp client: the reports, one module
docs/ # client-side docs, one per contract
skills/theme/ # theme skill: SKILL.md, template.json, validate.mjs
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 runs no commands: a plugin there is a module its server imports, so its client is the single file `opencode/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 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.

## Installation

Expand Down Expand Up @@ -78,6 +79,18 @@ curl -fsSL -o ~/.config/opencode/plugin/lich.js \

Per project instead of globally, use `.opencode/plugin/lich.js`. Updating means fetching the file again.

### omp (oh-my-pi)

omp has no marketplace of its own either, and its `--hook` and `--extension` flags are one list of modules. It scans `~/.omp/agent/extensions/` without being told, so dropping the file there is the install:

```bash
mkdir -p ~/.omp/agent/extensions
curl -fsSL -o ~/.omp/agent/extensions/lich.js \
https://raw.githubusercontent.com/omartelo/lich-plugin/main/omp/lich.js
```

To keep it somewhere else, name the path instead — `omp config set extensions '["/path/to/lich.js"]'`, which writes `extensions:` in `~/.omp/agent/config.yml` — or pass `--hook /path/to/lich.js` for a single run. Per project, `.omp/extensions/lich.js` works the same way. Updating means fetching the file again.

### Crush

Crush has no plugin system either: its hooks live in your own `crush.json`. Clone this repository, then merge [`hooks/crush-hooks.json`](hooks/crush-hooks.json) into `~/.config/crush/crush.json` (global) or the project's `crush.json`, replacing `<lich-plugin>` with the absolute path of the clone:
Expand Down Expand Up @@ -131,4 +144,4 @@ claude --plugin-dir .
node --test tests/*.test.mjs
```

Every hook script runs as a real subprocess, from the command line its registration spells, against a stub HTTP server — and the body it POSTs is asserted against [lich's contract fixtures](https://github.com/omartelo/lich/tree/main/docs/hooks/fixtures): an accepted shape, never a rejected one, the right endpoint and token, plus the client rules (no lich environment → no report; exit 0 when lich answers 500 or refuses the connection). The opencode module is imported instead of spawned and fed the events a real opencode run emits, against the same fixtures. Both share `tests/contract.mjs`. The fixtures are vendored in `tests/fixtures/` by `tests/refresh-fixtures.sh`; CI diffs them against upstream so a contract that moves in lich goes red here.
Every hook script runs as a real subprocess, from the command line its registration spells, against a stub HTTP server — and the body it POSTs is asserted against [lich's contract fixtures](https://github.com/omartelo/lich/tree/main/docs/hooks/fixtures): an accepted shape, never a rejected one, the right endpoint and token, plus the client rules (no lich environment → no report; exit 0 when lich answers 500 or refuses the connection). The opencode and omp modules are imported instead of spawned and fed the events a real run of each emits, against the same fixtures. All three share `tests/contract.mjs`. The fixtures are vendored in `tests/fixtures/` by `tests/refresh-fixtures.sh`; CI diffs them against upstream so a contract that moves in lich goes red here.
Loading
Loading