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

### Added

- **The prompt footer shows the session's errands.** Who it owes an answer, the
tasks it handed out that are still running (and how many wait on a human),
and the answers waiting to be collected: `lich: owes "docs" an answer, 2
tasks out (1 waiting), 1 answer to collect`. Reading never collects an
answer. While it shows, the worker count is left out, since it already counts
every worker. Needs a lich that takes the mod-status contract; an older one
shows nothing.

- **The edit guard tells you too, not only the model.** When a file you edit
was also edited by another lich session in the last 10 minutes, the status
line under the prompt reads `lich: app.js also edited by "quiet-comet"` until
that edit leaves the window. It shares the line with the worker count:
was also edited by another lich session in the last 10 minutes, the prompt
footer reads `lich: app.js also edited by "quiet-comet"` until that edit
leaves the window. It shares the line with the worker count:
`lich: 2 workers · app.js also edited by "quiet-comet"`.

### Changed

- **The lich line moved from the yellow warning to the prompt footer.** The
worker count and the notes above are drawn dim at the footer's right, beside
Claude Code's own mode labels, instead of as a `⚠` warning under the prompt.

## [0.17.0] - 2026-10-05

### Added
Expand Down
17 changes: 11 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,14 @@ hooks/crush-hooks.json # hook registration, Crush (hand-merged into c
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)
hooks/lich.js # the one module hooks/hooks.json names under `modules`: registers the five mods below
hooks/lich.js # the one module hooks/hooks.json names under `modules`: registers the six mods below
hooks/mod-control.js # Claude Code mod: lich drives the session
hooks/agent-cards.js # Claude Code mod: a general-purpose subagent runs as a lich session
hooks/mod-usage.js # Claude Code mod: the session reports its own context, rate limits and cost
hooks/edit-guard.js # Claude Code mod: an edit says which other lich session edited the file
hooks/worker-answer.js # Claude Code mod: a lich subagent worker's last message answers its task
hooks/status-line.js # the one status line Claude Code gives the plugin, shared by the mods above
hooks/mod-status.js # Claude Code mod: the session's relay errands in the prompt footer
hooks/status-line.js # the lich line in the prompt footer, shared by the mods above
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
Expand Down Expand Up @@ -63,6 +64,7 @@ Contracts are **canonical in the lich repository** (`docs/hooks/` there); this p
- [docs/agent-cards.md](docs/agent-cards.md): a general-purpose subagent Claude Code starts runs as a lich session, through the `lich open` CLI rather than an HTTP contract; Claude Code only
- [docs/mod-usage.md](docs/mod-usage.md): a Claude Code session reports the context window, rate limits and cost it measured itself, through a mod hooking `session.measure`; Claude Code only
- [docs/mod-answer.md](docs/mod-answer.md): a lich subagent worker answers the task it was handed with its turn's final message, through a mod reading the main loop's `classic.Stop` and `turn.complete`; Claude Code only
- [docs/mod-status.md](docs/mod-status.md): a Claude Code session shows the relay errands it is part of (owed, out, ready) in its prompt footer, through a mod reading `/mod/status`, which never collects; Claude Code only
- [docs/edit-guard.md](docs/edit-guard.md): an edit to a file another lich session edited in the last minutes carries a note naming it, through markers in the checkout's git dir rather than an HTTP contract; Claude Code only
- [docs/providers.md](docs/providers.md) — the per-harness map, including what installing on omp takes and what it cannot report

Expand Down Expand Up @@ -97,14 +99,16 @@ lich it subscribes to nothing at all.
Claude Code mod, handing it a fake `$` and asserting its acks against the
mod-control fixtures and its polls against the commands lich sends;
[tests/mod-usage.test.mjs](tests/mod-usage.test.mjs) does the same for its
usage reports against the mod-usage fixtures, and
usage reports against the mod-usage fixtures,
[tests/worker-answer.test.mjs](tests/worker-answer.test.mjs) for a worker's
answer against the mod-answer fixtures.
answer against the mod-answer fixtures, and
[tests/mod-status.test.mjs](tests/mod-status.test.mjs) for the footer line it
draws from the mod-status response.
[tests/edit-guard.test.mjs](tests/edit-guard.test.mjs) hands the edit guard a
fake `$` with an in-memory git dir and asserts the markers it leaves and the
note another session's marker adds, and
[tests/status-line.test.mjs](tests/status-line.test.mjs) that the mods share
the one status line Claude Code gives the plugin.
one lich line in the prompt footer.
[tests/agent-cards.test.mjs](tests/agent-cards.test.mjs) hands the agent-cards
mod the same kind of fake `$` and asserts the `lich open` argv
it runs, the Agent result it answers each outcome with, and that a call it does
Expand All @@ -115,6 +119,7 @@ dependencies.
[tests/mod-control.engine.test.ts](tests/mod-control.engine.test.ts) and
[tests/agent-cards.engine.test.ts](tests/agent-cards.engine.test.ts) and
[tests/mod-usage.engine.test.ts](tests/mod-usage.engine.test.ts) and
[tests/mod-status.engine.test.ts](tests/mod-status.engine.test.ts) and
[tests/worker-answer.engine.test.ts](tests/worker-answer.engine.test.ts) and
[tests/edit-guard.engine.test.ts](tests/edit-guard.engine.test.ts) run the
mods under Claude Code's own engine (`claude plugin test .`); they need a
Expand Down Expand Up @@ -168,6 +173,6 @@ Same standard as the `lich` repository (Keep a Changelog + SemVer). Releases are

1. Move the `[Unreleased]` entries in `CHANGELOG.md` under the new version heading (with date) and refresh the compare links at the bottom.
2. Pick the number by what changed: a release that sends anything lich's contract did not already accept (a new endpoint, header, field or value) bumps the **minor** (the major, from 1.0); anything else bumps the **patch**. lich accepts every release below the next minor of the contract it implements, so a contract change released as a patch reaches a lich that refuses it. `docs/hooks/README.md` in lich, Versioning, is the rule.
3. Align `version` in `.claude-plugin/plugin.json` **and** `.codex-plugin/plugin.json` with the tag, and `plugin_version` in each `hooks/report-*.sh` and `PLUGIN_VERSION` in `opencode/lich.js`, `omp/lich.js`, `hooks/mod-control.js`, `hooks/mod-usage.js` and `hooks/worker-answer.js`, the header every report sends. The suite fails while any of them disagrees with the Claude Code manifest. The root `plugin.json` is not a third one to bump: Antigravity's manifest takes only a `name`, and lich writes its own carrying the tag when it installs a release.
3. Align `version` in `.claude-plugin/plugin.json` **and** `.codex-plugin/plugin.json` with the tag, and `plugin_version` in each `hooks/report-*.sh` and `PLUGIN_VERSION` in `opencode/lich.js`, `omp/lich.js`, `hooks/mod-control.js`, `hooks/mod-usage.js`, `hooks/mod-status.js` and `hooks/worker-answer.js`, the header every report sends. The suite fails while any of them disagrees with the Claude Code manifest. The root `plugin.json` is not a third one to bump: Antigravity's manifest takes only a `name`, and lich writes its own carrying the tag when it installs a release.
4. Annotated tag `vX.Y.Z` + push with the tag.
5. `gh release create vX.Y.Z` with the notes taken from the matching `CHANGELOG.md` section.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,13 @@ hooks/report-tool.sh # session-state hook: the tool a turn is runni
hooks/report-session-start.sh # session-start hook
hooks/report-title.sh # session-title hook
hooks/report-touched.sh # session-touched hook
hooks/lich.js # the Claude Code mods' entry, which registers the five below
hooks/lich.js # the Claude Code mods' entry, which registers the six below
hooks/mod-control.js # mod-control client, a Claude Code mod
hooks/agent-cards.js # a general-purpose subagent as a lich session, a Claude Code mod
hooks/mod-usage.js # mod-usage client, a Claude Code mod
hooks/edit-guard.js # names the other lich session that edited a file, a Claude Code mod
hooks/worker-answer.js # mod-answer client: a lich worker's last message answers its task, a Claude Code mod
hooks/mod-status.js # mod-status client: the session's errands in the prompt footer, a Claude Code mod
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
Expand Down
11 changes: 6 additions & 5 deletions docs/agent-cards.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ Nothing runs after that: the delivery `lich open` printed decides the answer.

| delivery | the Agent call answers |
|-------------------------------|---------------------------------------------------------------------|
| `pending` | `async_launched` at once, plus a note on how the report comes back; the worker is counted in the status line |
| `pending` | `async_launched` at once, plus a note on how the report comes back; the worker is counted in the footer |
| `answered` | `completed`: the report, then the checkout (and branch) it is on |
| `unanswered` | `async_launched`, as for `pending`: a turn that ended unanswered usually left a background command running, and the report still arrives as a [lich] note |
| `unread`, `undelivered` | denied: the task never reached the card |
Expand Down Expand Up @@ -110,10 +110,11 @@ for an isolated call.
committed.** Uncommitted
changes are not in the worker's checkout. On a detached HEAD, or when git
cannot answer, the base is left to lich, which uses the project's current branch.
- **Claude Code's status line counts the workers still running**, as its own
"N agents" hint counts background agents: `N workers` under the prompt
(`$.ui.status`), which Claude Code shows after the plugin's name as
`lich: N workers`, cleared at none. Every 5 seconds while any runs, the mod reads
- **The prompt footer counts the workers still running**, as Claude Code's own
"N agents" hint counts background agents: `lich: N workers` in the lich line
of the footer (`hooks/status-line.js`, see [mod-status.md](mod-status.md)),
cleared at none, and left out while that line shows lich's errands, which
count every worker. Every 5 seconds while any runs, the mod reads
`$LICH_BIN sessions --json`. A worker in this checkout is finished once lich
no longer lists it: lich closes it after it reported and its turn ended
(`lich open --subagent` in lich's docs/cli.md), while a turn it ended with a
Expand Down
9 changes: 4 additions & 5 deletions docs/edit-guard.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,11 @@ unless `LICH_SESSION_ID` is set.
after the tool's result: the path, the session, the time and how long
ago, and that `send_to_session` or `lich send` reaches that session. A
refused call carries the line too, since it explains the refusal.
6. The same edit puts `<file name> also edited by <session>` in the status
line under the prompt, for the user, who never reads the `context` line.
6. The same edit puts `<file name> also edited by <session>` in the lich line
of the prompt footer, for the user, who never reads the `context` line.
It stays until the marker leaves the 10-minute window, or a newer one
replaces it. Claude Code keeps one status line per plugin, so it shares the
line with the worker count of [agent-cards.md](agent-cards.md), through
`hooks/status-line.js`.
replaces it. The line is shared with the other mods through
`hooks/status-line.js` ([mod-status.md](mod-status.md), What the line reads).

The line names the session by the label on its card, the name the user and
`lich send` know it by. lich puts no label in a session's environment and no
Expand Down
62 changes: 62 additions & 0 deletions docs/mod-status.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Mod: status

Client side of the [mod-status contract](https://github.com/omartelo/lich/blob/main/docs/hooks/mod-status.md)
(the endpoint and the response are defined in the lich repository; this plugin
only implements them).

Shows, in the prompt footer, the relay errands the session is
part of: who it owes an answer, the tasks it handed out that are still running,
and the answers waiting to be collected. The card shows these in the window;
this puts them in front of the person at the terminal.

| client | Claude Code | Codex | Antigravity | opencode | omp | Crush |
|-----------------------|---------------------------------------------------------|---------------------|---------------------|---------------------|---------------------|---------------------|
| `hooks/mod-status.js` | mod, registered by `hooks/lich.js`, the `modules` entry | none: no mod system | none: no mod system | none: no mod system | none: no mod system | none: no mod system |

| when | read |
|---------------------------------------------|----------------------------------------------------------|
| `session.start`, then every 5 seconds | `GET /mod/status?token=…&session_id=…`, `X-Lich-Plugin` |

## What the line reads

Each list that is not empty adds a piece, in this order, joined by commas:

| list | piece |
|---------|-----------------------------------------------------------------------------------------|
| `owed` | `owes "Session 26" an answer` for one asked by a session, `owes N answers` otherwise |
| `open` | `N tasks out`, plus `(M waiting)` for those whose session waits on a human |
| `ready` | `N answers to collect` |

The line reads `lich: 1 task out, 1 answer to collect`. With all three lists
empty the piece is gone.

The mods share one lich line, built by `hooks/status-line.js`: the errands,
then the worker count of [agent-cards.md](agent-cards.md), then the note of
[edit-guard.md](edit-guard.md), joined by ` · `. While the errands show, the
worker count is left out: every worker that mod opens is an errand of this
session, so `open` already counts it. The line is drawn dim among the mode
labels at the right of the prompt footer, by a `ui.render` hook on
`SessionMode` that adds it after Claude Code's own labels. `$.ui.status` is
not used: Claude Code draws it as a yellow warning under the prompt, and keeps
only one per plugin.

## How it behaves

- **Outside lich it never reads.** It reads the three variables in
`session.start` and stops there when one is missing.
- **Only an interactive session reads.** A `claude -p` run from a tool inside a
lich session inherits that session's variables, and would show the parent's
errands. Its `session.start` hook matches `isInteractive: true`.
- **Reading never collects.** An answer under `ready` stays in the inbox for
the agent's `wait_for_answer`.
- **A failed read is not retried.** The piece is cleared until a read works
again, 5 seconds later.
- **A `404` stops the reads for the session**: that lich predates the contract.

## Limits

- **Up to 5 seconds late.** lich pushes nothing; an errand that opens or
closes shows at the next read.
- **Labels are the ones at send time**, as lich keeps them.
- **Claude Code only.** Every other harness has no mod system; there the card
is the only place these errands show.
1 change: 1 addition & 0 deletions docs/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ This repository is one plugin, packaged for each harness it supports:
| `hooks/mod-control.js` | Claude Code | the mod-control client, a mod |
| `hooks/agent-cards.js` | Claude Code | runs a general-purpose subagent as a lich session, a mod |
| `hooks/mod-usage.js` | Claude Code | the mod-usage client, a mod |
| `hooks/mod-status.js` | Claude Code | the mod-status client, a mod |
| `hooks/edit-guard.js` | Claude Code | names the other lich session that edited a file, a mod |
| `hooks/worker-answer.js` | Claude Code | the mod-answer client: a worker's last message answers its task, a mod |
| `opencode/lich.js` | opencode | the whole client, as a module |
Expand Down
11 changes: 5 additions & 6 deletions hooks/agent-cards.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
// Every function that takes `$` is declared at the top level: the loader
// refuses a module that hands `$` to a nested function or keeps it in a variable.

import { statusLineWith } from "./status-line.js"
import { setStatusPart } from "./status-line.js"

// lich's `promptLimit` (internal/relay). lich checks it only after the session
// was opened, so a task over it would leave an empty card behind.
Expand Down Expand Up @@ -326,17 +326,16 @@ async function runAsSession($, state, e, next) {
}

/**
* Pins how many workers this session waits on under its prompt, as Claude
* Code's own hint does for its background agents, and clears it at none.
* Claude Code puts the plugin's name before the line, so it reads
* "lich: 2 workers" (measured on 2.1.289).
* Shows how many workers this session waits on in the footer's lich line, as
* Claude Code's own hint does for its background agents, and clears it at none.
*
* @param {Engine} $
* @param {State} state
*/
function showWorkers($, state) {
const count = state.workers.size
$.ui.status(statusLineWith("workers", count === 0 ? undefined : `${count} worker${count === 1 ? "" : "s"}`))
setStatusPart("workers", count === 0 ? undefined : `${count} worker${count === 1 ? "" : "s"}`)
$.ui.invalidate("ui.render")
}

/**
Expand Down
8 changes: 5 additions & 3 deletions hooks/edit-guard.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
// Every function that takes `$` is declared here at the top level: the loader
// refuses a module that hands `$` to a nested function.

import { statusLineWith } from "./status-line.js"
import { setStatusPart } from "./status-line.js"

// Long enough to span the other session's turn that made the edit, short
// enough that a marker from work long finished stops raising notes.
Expand Down Expand Up @@ -147,7 +147,8 @@ let shownMarker
function showEditBy($, marker, who) {
shownMarker = marker
const name = marker.path.slice(Math.max(marker.path.lastIndexOf("/"), marker.path.lastIndexOf("\\")) + 1)
$.ui.status(statusLineWith("edits", `${name} also edited by ${who}`))
setStatusPart("edits", `${name} also edited by ${who}`)
$.ui.invalidate("ui.render")
}

/**
Expand All @@ -157,7 +158,8 @@ function showEditBy($, marker, who) {
function clearEditBy($, marker) {
if (shownMarker !== marker) return
shownMarker = undefined
$.ui.status(statusLineWith("edits", undefined))
setStatusPart("edits", undefined)
$.ui.invalidate("ui.render")
}

/** @param {Engine} $ @param {unknown} error */
Expand Down
Loading
Loading