Client side of the mod-answer contract (the endpoint and the payload are defined in the lich repository; this plugin only implements them).
Makes a lich subagent worker (a session opened with lich open --subagent,
which is what agent-cards opens for an Agent call) answer the
task it was handed the way a native subagent does: the final message of its
turn is its result. lich hands such a worker its task with no ticket and no
reply instructions, and answers the errand with what this mod posts.
| client | Claude Code | Codex | Antigravity | opencode | omp | Crush |
|---|---|---|---|---|---|---|
hooks/worker-answer.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 |
| event | sent |
|---|---|
classic.Stop, then turn.complete{reason=answer} |
POST /mod/answer with {"session_id", "text"} |
classic.Stop with a blank message, then turn.complete{reason=answer} |
POST /mod/answer with {"session_id", "unanswered": "blank"} |
turn.complete{reason=answer} in a session that never saw a classic.Stop |
the same two bodies, from the completion's answer |
- Only a worker answers. It reads
LICH_SUBAGENT_DEPTHinsession.start, which lich sets on every Claude Code session it starts, and reports only when it is an integer above0: a session openedn--subagentlevels down carriesn, any other0. Under a lich older than that variable it readsLICH_SUBAGENT_CARDSinstead and reports only when it isoff, which that lich sets on every--subagentsession. A session the user turned subagent cards off for carries it too; lich ignores a report from a session with no subagent errand open. - A turn that handed work to the background does not answer. Its main-loop
classic.Stoplists what is still running inbackground_tasks(a shell, a subagent, a monitor, a workflow); Claude Code resumes in a new turn when that work finishes, and that turn'sStop, with nothing listed, is the one that answers. - A blank turn says so. One whose final message is blank posts
unanswered: "blank", so the caller hears the errand ended instead of waiting on it, and only when the turn's ownclassic.Stoplists nothing running. The plugin reports blank turns only. - An aborted turn does not report: its user took the worker over, and the turn they start next is the one that answers.
- A refused or failed turn does not report either, though the contract has
unanswered: "refusal"and"error"for them. Measured on Claude Code 2.1.296, both end throughStopFailure(a refusal with errorinvalid_request) and thenturn.complete, with noStop, and neither carriesbackground_tasks, nor does any mod engine call list them: the mod cannot tell a failed turn with nothing running from one whose background work resumes it later and answers then, so a report would answer the errand twice. Both are pending until Claude Code exposes that list on the failure. turn.completeis matched once perreason(answer,refusal,error,aborted), which keeps the hooks beside mod-control's ownturn.completehook: Claude Code refuses one plugin's second hook on an event with no matcher. Every one of them drops theStopit read.- A worker whose
classic.Stopis bypassed answers fromturn.complete. Measured on Claude Code 2.1.306: on a machine with managed settings (Team and Enterprise organizations) the builtincc-plugin-sec-defaultseats itself outermost and sendsclassic.Stopon past the plugins a person installs; the engine marks the mod's hookskipped("bypassed by") and tells the model, but no mod call says so. The mod infers it from a session that has not seen a singleclassic.Stop: one main-loopturn.completewith reasonanswerthen posts itsanswer, orunanswered: "blank"for a blank one. The bypass follows the settings the session started with, so once aStophas been seen the Stop rule above holds for the rest of the session. - Without a
Stop, only a subagent can hold the answer back. There is nobackground_tasksto read.$.agent.list()names the session's subagents (not its shells, monitors or workflows), and one listedrunningorwaiting, or a list that cannot be read, holds the turn's report. A turn that handed a background shell, monitor or workflow off answers with what it said when it did, and lich takes the first answer, so the resumed turn's answer is lost. Pending until the engine exposes the background tasks, or the bypass, to a mod. - A subagent's turn inside the worker does not answer.
classic.Stopfires on the main loop only, and aturn.completethat carries anagentIdis skipped. - A Stop answers only for its own turn. The text is posted when the
completion's
answerequals the Stop'slast_assistant_message, a blank Stop only beside a blankanswer, and the Stop is dropped once a completion read it: an aborted turn fires no Stop, and must not inherit the last one. - The text is cut at 16,000 UTF-16 units and marked
[truncated], as anask's answer is. - The turn is never held up. The answer is posted beside the chain; one that fails is dropped, never retried.
- Outside lich, and in a non-interactive run, it never reports.
Measured on Claude Code 2.1.289: classic.Stop fires on the main loop before
turn.complete, its last_assistant_message equals the completion's answer,
and its background_tasks is [] when nothing runs. classic.SubagentStop
fires for engine forks too, and a background subagent lists itself as still
running there, which is why only the main loop's Stop is read. A worker that
asks with AskUserQuestion blocks with no Stop until someone answers in its
card.