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
13 changes: 7 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **A general-purpose subagent runs as a lich session you can watch and steer.**
Inside lich, the Claude Code mod opens each general-purpose subagent (or one
started with no type) as a Claude Code session on a branch of its own, off the
asking session's branch, and hands its report back as the subagent's result.
Explore, Plan and other types stay inside Claude Code. When lich cannot take
the task, the subagent runs inside Claude Code as before, with a toast saying
why. Esc stops the wait, not the session; its report then arrives as a
[lich] note. Needs Claude Code 2.1.280 or later with mods on (measured on
2.1.289).
asking session's branch. The Agent call returns at once as a background
subagent, so the asking session keeps working, and the report arrives at its
prompt as a [lich] note. Explore, Plan and other types stay inside Claude
Code. When lich cannot take the task, the subagent runs inside Claude Code as
before, with a toast saying why. Turning off "Subagents as lich sessions" in
lich's Settings › Providers › Claude Code keeps every subagent inside Claude
Code. Needs Claude Code 2.1.280 or later with mods on (measured on 2.1.289).
- **lich can ask a Claude Code session a side question without stopping it.**
The mod answers an `ask` with a fork of the session's own conversation, which
runs beside the turn, stays out of the conversation and is mostly served from
Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ Contracts are **canonical in the lich repository** (`docs/hooks/` there); this p
- [docs/session-title.md](docs/session-title.md) — the provider's own session title, via `PostToolUse`/`PreInvocation` and `Stop`
- [docs/session-touched.md](docs/session-touched.md) — git-status refresh signal via `PostToolUse` (file-mutating tools only)
- [docs/mod-control.md](docs/mod-control.md): lich drives a Claude Code session (prompt, abort, model, effort, command) and asks it side questions (ask) through a mod that long-polls for commands and acks each one; Claude Code only, the other harnesses have no mod system
- [docs/agent-cards.md](docs/agent-cards.md): a general-purpose subagent Claude Code starts runs as a lich session, through the `lich open` and `lich wait` CLI rather than an HTTP contract; Claude Code only
- [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/providers.md](docs/providers.md) — the per-harness map, including what installing on omp takes and what it cannot report

Expand Down Expand Up @@ -94,7 +94,7 @@ 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.
[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` and `lich wait` argv
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
not take, or one lich cannot take, reaches the native agent untouched.
[tests/contract.mjs](tests/contract.mjs) is the fixtures, the assertions and the
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ run it. No other harness has a mod system, so none of them gets the controls.

The same mod system turns a **general-purpose subagent** Claude Code starts into
a lich session: its own worktree, branched off the asking session's branch, and
a card you can watch and steer. The Agent call waits for that session's report
and hands it back as the subagent's result; Explore, Plan and other agent types
stay inside Claude Code. [docs/agent-cards.md](docs/agent-cards.md) has the
a card you can watch and steer. The Agent call returns at once as a background
subagent, and the session's report arrives at the asking session's prompt as a
`[lich]` note; Explore, Plan and other agent types stay inside Claude Code. [docs/agent-cards.md](docs/agent-cards.md) has the
rules and what happens when lich cannot be reached.

It also ships skills for the parts of lich you configure from inside a session:
Expand Down
77 changes: 45 additions & 32 deletions docs/agent-cards.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ card the user can watch and steer, where Claude Code would otherwise run the
agent hidden inside the asking session.

There is no HTTP contract behind it. It drives the lich CLI the asking session
already has, `$LICH_BIN open` and `$LICH_BIN wait`, so what it reads is what
those commands print (`docs/cli.md` in the lich repository: `open --json`,
`wait --json`, Exit status).
already has, `$LICH_BIN open`, so what it reads is what that command prints
(`docs/cli.md` in the lich repository: `open --json`, Exit status).

| client | Claude Code | Codex | Antigravity | opencode | omp | Crush |
|------------------------|-------------------------------------------------------|---------------------|---------------------|---------------------|---------------------|---------------------|
Expand All @@ -30,51 +29,62 @@ of these hold, and every other one goes on to Claude Code untouched:
- the session is interactive (`session.start` saw `isInteractive`), since a
`claude -p` started from a tool inside a lich session inherits its variables;
- `LICH_BIN` and `LICH_SESSION_ID` are set, which is to say the session runs
inside lich.
inside lich;
- `LICH_SUBAGENT_CARDS` is not `off`. lich sets it to `off` in a Claude Code
session it starts while "Subagents as lich sessions" is off in Settings ›
Providers › Claude Code, and leaves it out otherwise, so the default is on.
A session's environment is fixed when it starts, so turning the setting over
reaches the sessions started after that.

A taken call runs:

1. `git branch --show-current` in the session's directory, for the base.
2. `$LICH_BIN open --kind claude --worktree <branch> [--base <current>] [--model <model>] --prompt <prompt> --json`,
given two minutes, the sum of the waits lich's own client allows an open
with a task.
3. While the delivery is `pending`, `$LICH_BIN wait --timeout 540 --json <ticket>`,
each given ten minutes, which is the most `$.process.run` allows.

| outcome | the Agent call answers |
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 |
| `answered` | `completed`: the report, then the branch and checkout it is on |
| `unanswered` | `completed`: the worker ended its turn without reporting, and where to look |
| `unread`, `undelivered` | denied: the task never reached the card |
| a status it does not know | denied, naming the card and its branch |
| the open failed | the native agent runs, with a toast saying why |
| the wait failed, or a status it does not know | denied, naming the card and its branch |

The `completed` result is the Agent tool's own arm: `agentId` is the worker's
roster name, `worktreePath` and `worktreeBranch` are its checkout, and the token
and tool counts are zero because they are the worker's session's, not this one's.
`pending` is the usual case: a worker rarely reports within the open's own
wait. Both results are the Agent tool's own arms, and `agentId` is the worker's
roster name in each. `async_launched` carries the call's description and prompt
and an empty `outputFile`, which is the value Claude Code gives its own agents
that have no output file: there is nothing to read, and without
`canReadOutputFile` Claude Code never shows the model the field. `completed`
carries the worker's checkout in `worktreePath` and `worktreeBranch`, and token
and tool counts of zero because they are the worker's session's, not this one's.

## How it behaves

- **The call is held until the worker reports**, as a foreground subagent holds
it: the asking turn waits. Claude Code's hook budget does not count the time a
`$.process.run` is in flight, so a held call can last as long as the worker
does (measured past 12 minutes on 2.1.289). Parallel Agent calls run their
hooks concurrently, so each opens its own card at once. Answering at once
(`async_launched`) was turned down: it promises a background task Claude Code
can notify about, read and stop, and none of that exists for a lich session.
- **The call runs in the background**, as a background subagent does: once lich
has the task it answers `async_launched` and the asking turn goes on. Parallel
Agent calls run their hooks concurrently, so each opens its own card at once.
- **The report comes back through lich, not Claude Code.** The task is an
ordinary lich errand (not `--private`), so when the worker replies lich types
a `[lich]` note at the asking session's prompt (through mod-control's `prompt`
when the session runs the mod), and the model collects it with
`wait_for_answer` or `lich wait`. Claude Code's own text for a background
agent promises a task notification and names `SendMessage`, so the result
carries a `context` line the model reads after it: where the worker is, how
its report arrives, its ticket, and that `send_to_session` or `lich send`
reaches it where `SendMessage` cannot.
- **It falls back to the native agent only before lich has the task.** Claude
Code runs the native agent in place of any hook that throws, overruns its
budget or lets a `$.process.run` reject, so the module catches every failure
and decides. A task over lich's 8192-byte limit, or an open that failed, runs
natively with a toast saying why, naming the card when one was opened. Once
the task reached the worker, a failure denies the call instead: a native agent
beside the worker would do the work twice. A worker's later report still
arrives at the asking session's prompt as a `[lich]` note.
- **Esc stops the wait, never the worker.** Claude Code aborts the call and
ends the `lich wait` child; the worker keeps running on its card, its ticket is
no longer waited on, and its report arrives as a `[lich]` note. Esc while the
open is still running denies the call too, because lich may already have
handed the task over.
natively with a toast saying why, naming the card when one was opened. Esc
while the open is still running denies the call instead, because lich may
already have handed the task over and a native agent beside the worker would
do the work twice; a report that worker sends still arrives as a `[lich]` note.
- **The branch is named from the call.** The description is slugged the way
lich's own worktree dialog slugs a typed name (2 to 5 words, 10 to 40
characters, letters and digits in any script), `agent` when nothing is left,
Expand All @@ -89,9 +99,6 @@ and tool counts are zero because they are the worker's session's, not this one's
- **A model the call names is the worker's model**, passed as `--model`. With
none, the worker starts on the model lich opens Claude Code with, not the
asking session's, which a native agent would inherit.
- **The worker is reached through lich.** The result tells the model to use
`send_to_session` or `lich send`: Claude Code's hand-back suggests
`SendMessage`, which cannot reach a lich session.

## Known ceilings

Expand All @@ -101,7 +108,13 @@ and tool counts are zero because they are the worker's session's, not this one's
so they stay native.
- **The result reports no tokens and no tool uses.** Both are counted in the
worker's own session.
- **Claude Code does not track the worker as a task.** It is not in the
background-task list, Claude Code sends no completion notification for it,
and stopping it is done on its card, not from Claude Code.
- **A report nobody collects within an hour is lost.** lich drops an
unanswered ticket an hour after anyone last waited on it, and a result left
in the inbox on the same clock; the work itself stays on the worker's card
and branch.
- **A reload of the module mid-session forgets that the session is
interactive** until its next `session.start`, and Agent calls run natively
until then. Claude Code defers a reload while a turn runs, so a held call is
never cut by one.
until then.
5 changes: 4 additions & 1 deletion docs/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ lich drives an agent CLI inside a PTY and injects the same three variables
(`LICH_PORT`, `LICH_TOKEN`, `LICH_SESSION_ID`) into every one it spawns. The
[hook contracts](https://github.com/omartelo/lich/blob/main/docs/hooks/README.md)
are therefore provider-agnostic: what changes per provider is only *how the
harness registers hooks* and *what its lifecycle events are called*.
harness registers hooks* and *what its lifecycle events are called*. A Claude
Code session also carries `LICH_BIN`, and `LICH_SUBAGENT_CARDS=off` when
"Subagents as lich sessions" is off in lich's Settings › Providers › Claude
Code; both are read only by the agent-cards mod ([agent-cards.md](agent-cards.md)).

This repository is one plugin, packaged for each harness it supports:

Expand Down
Loading
Loading