moonshine-listen — authorship feedback listener (Claude Code adapter)
This is the idle-coverage half of the moonshine authorship-feedback loop
(plugins/moonshine/FEEDBACK.md). The Stop hook already handles delivery at
turn boundaries; this skill covers requests that land while the session is
idle, and powers the HUD's live status (the heartbeat) and its mode controls
(the control.json it reads). Note the default mode is accumulate:
comments pile up and are only drained when the author presses Address in the
HUD (address.json) or has opted into continuous auto-address (listen).
Each invocation performs one tick. Run it under /loop so the ticks repeat.
Scope
- If invoked with a project name argument, operate on
~/.agent/moonshine/<arg>/.feedback/ only.
- Otherwise operate on every existing
~/.agent/moonshine/*/.feedback/ inbox.
- If
MOONSHINE_FEEDBACK=off is set in the environment, do nothing and end
the loop — the same kill switch the Stop hook and the dev server honor.
Use absolute, ISO-8601 UTC timestamps (date -u +%Y-%m-%dT%H:%M:%SZ) everywhere.
One tick
For each in-scope .feedback/ directory:
Manifest. If adapter.json is missing, write
{"harness":"claude-code","version":"adapter","installedAt":"<now>"}.
Read control. Read control.json; treat a missing file as
{"mode":"accumulate"} (the default: hold comments, don't drain).
- If
mode == "stopped" and there is no address.json: write a final heartbeat.json with
"mode":"stopped" and the current ts, then end — do not schedule
another tick for this project. (If all in-scope projects are stopped, end
the loop entirely; tell the user the listener stopped.)
Route to the authoring session. Read the optional sessionId from
address.json (for a one-shot request), or from control.json when mode is
listen. Compare it with $CLAUDE_CODE_SESSION_ID. If the request names a
different session — or names one but the current session ID is unavailable
— skip this project entirely and do not consume its request. Legacy files
without a sessionId remain unscoped.
Heartbeat. Write heartbeat.json:
{"harness":"claude-code","mode":"<mode>","ts":"<now>","intervalSec":90,"pending":<n>}
where <n> is the number of status:"pending" comment files.
Drain (only when the author asked). Drain when mode == "listen"
(continuous auto-address), or whenever address.json exists (the HUD's
explicit one-shot Address request). Address is a deliberate override of
pause/stop: drain once while leaving the underlying control mode unchanged.
After a drain pass
triggered by address.json, delete address.json — the request is
consumed. When mode == "accumulate" and there is no address.json, skip
draining entirely: comments accumulate by design.
For each comment file (any *.json other than control.json /
heartbeat.json / adapter.json / address.json) that
is either status == "pending" or status == "delivered" with a
deliveredAt more than 300s ago (a comment claimed by an earlier
turn/tick that was never addressed — re-surface it rather than strand it):
- Claim it exclusively by first renaming the record itself to a private name
(
mv <id>.json <id>.json.claiming.$$). That rename is the mutual-exclusion
point — if another drainer already took it, your mv fails and you skip
the comment. Renaming a rewritten copy over the original is NOT a claim
(both racers would win). Then rewrite the claimed file with
status:"delivered" and deliveredAt:"<now>" and rename it back to
<id>.json.
- Read
target (path, kind, figureId, range, excerpt, anchorHash)
and comment. The source file is <project>/content/<target.path>.
- If
anchorHash no longer matches the current file body, the prose moved —
locate the passage by excerpt instead of trusting range.
- Address the comment by editing the source file (or the figure
component / registry it points at).
- Record the outcome: set
status:"addressed", addressedAt:"<now>", and a
one-line reply summarizing what you changed. Write atomically.
- When
mode == "paused" or "stopped" and there is no address request,
skip draining. If stopped with an address request, drain once, write the
final stopped heartbeat, and then end this project's loop.
Continue the loop
After the tick, unless every in-scope project was stopped:
- When running under
/loop (dynamic): call ScheduleWakeup with
delaySeconds: 90 and the same prompt ($moonshine-listen) so the next tick
fires. Keep intervalSec in the heartbeat aligned with this delay. If the
harness has no ScheduleWakeup tool, fall back to telling the author to
re-invoke the skill (or run it under /loop).
- If you are not in a loop, tell the author to run
/loop /moonshine:moonshine-listen for continuous listening; a bare
invocation only does a single tick. (Plugin skills are namespaced — the bare
/moonshine-listen does not resolve.)
Notes
- The Stop hook and this loop both claim by atomic rename, so they never
double-process a comment even if they run close together.
- Both paths honor a request's
sessionId, so another Claude session cannot
steal feedback intended for the article's authoring session.
- Keep replies short and factual — they surface back in the author's HUD next
to their original comment.
1---2name: moonshine-listen3description: Listen for moonshine authorship feedback and address comments on an interval. Invoke when the author asks you to watch for feedback from the article HUD, or when prompted to start the listener. Best run under /loop (e.g. `/loop $moonshine-listen`) so it keeps ticking while the session is idle.4---56# moonshine-listen — authorship feedback listener (Claude Code adapter)78This is the **idle-coverage half** of the moonshine authorship-feedback loop9(`plugins/moonshine/FEEDBACK.md`). The Stop hook already handles delivery at10turn boundaries; this skill covers requests that land while the session is11idle, and powers the HUD's live status (the heartbeat) and its mode controls12(the `control.json` it reads). Note the default mode is **accumulate**:13comments pile up and are only drained when the author presses Address in the14HUD (`address.json`) or has opted into continuous auto-address (`listen`).1516Each invocation performs **one tick**. Run it under `/loop` so the ticks repeat.1718## Scope1920- If invoked with a project name argument, operate on21 `~/.agent/moonshine/<arg>/.feedback/` only.22- Otherwise operate on every existing `~/.agent/moonshine/*/.feedback/` inbox.23- If `MOONSHINE_FEEDBACK=off` is set in the environment, do nothing and end24 the loop — the same kill switch the Stop hook and the dev server honor.2526Use absolute, ISO-8601 UTC timestamps (`date -u +%Y-%m-%dT%H:%M:%SZ`) everywhere.2728## One tick2930For each in-scope `.feedback/` directory:31321. **Manifest.** If `adapter.json` is missing, write33 `{"harness":"claude-code","version":"adapter","installedAt":"<now>"}`.34352. **Read control.** Read `control.json`; treat a missing file as36 `{"mode":"accumulate"}` (the default: hold comments, don't drain).37 - If `mode == "stopped"` and there is no `address.json`: write a final `heartbeat.json` with38 `"mode":"stopped"` and the current `ts`, then **end** — do not schedule39 another tick for this project. (If all in-scope projects are stopped, end40 the loop entirely; tell the user the listener stopped.)41423. **Route to the authoring session.** Read the optional `sessionId` from43 `address.json` (for a one-shot request), or from `control.json` when mode is44 `listen`. Compare it with `$CLAUDE_CODE_SESSION_ID`. If the request names a45 different session — or names one but the current session ID is unavailable46 — skip this project entirely and do not consume its request. Legacy files47 without a `sessionId` remain unscoped.48494. **Heartbeat.** Write `heartbeat.json`:50 ```json51 {"harness":"claude-code","mode":"<mode>","ts":"<now>","intervalSec":90,"pending":<n>}52 ```53 where `<n>` is the number of `status:"pending"` comment files.54555. **Drain (only when the author asked).** Drain when `mode == "listen"`56 (continuous auto-address), **or** whenever `address.json` exists (the HUD's57 explicit one-shot Address request). Address is a deliberate override of58 pause/stop: drain once while leaving the underlying control mode unchanged.59 After a drain pass60 triggered by `address.json`, delete `address.json` — the request is61 consumed. When `mode == "accumulate"` and there is no `address.json`, skip62 draining entirely: comments accumulate by design.63 For each comment file (any `*.json` other than `control.json` /64 `heartbeat.json` / `adapter.json` / `address.json`) that65 is either `status == "pending"` **or** `status == "delivered"` with a66 `deliveredAt` more than 300s ago (a comment claimed by an earlier67 turn/tick that was never addressed — re-surface it rather than strand it):68 - Claim it exclusively by first renaming the record itself to a private name69 (`mv <id>.json <id>.json.claiming.$$`). That rename is the mutual-exclusion70 point — if another drainer already took it, your `mv` fails and you skip71 the comment. Renaming a rewritten copy *over* the original is NOT a claim72 (both racers would win). Then rewrite the claimed file with73 `status:"delivered"` and `deliveredAt:"<now>"` and rename it back to74 `<id>.json`.75 - Read `target` (`path`, `kind`, `figureId`, `range`, `excerpt`, `anchorHash`)76 and `comment`. The source file is `<project>/content/<target.path>`.77 - If `anchorHash` no longer matches the current file body, the prose moved —78 locate the passage by `excerpt` instead of trusting `range`.79 - **Address the comment** by editing the source file (or the figure80 component / registry it points at).81 - Record the outcome: set `status:"addressed"`, `addressedAt:"<now>"`, and a82 one-line `reply` summarizing what you changed. Write atomically.83 - When `mode == "paused"` or `"stopped"` and there is no address request,84 skip draining. If stopped with an address request, drain once, write the85 final stopped heartbeat, and then end this project's loop.8687## Continue the loop8889After the tick, unless every in-scope project was `stopped`:90- When running under `/loop` (dynamic): call **ScheduleWakeup** with91 `delaySeconds: 90` and the same prompt (`$moonshine-listen`) so the next tick92 fires. Keep `intervalSec` in the heartbeat aligned with this delay. If the93 harness has no ScheduleWakeup tool, fall back to telling the author to94 re-invoke the skill (or run it under `/loop`).95- If you are not in a loop, tell the author to run96 `/loop /moonshine:moonshine-listen` for continuous listening; a bare97 invocation only does a single tick. (Plugin skills are namespaced — the bare98 `/moonshine-listen` does not resolve.)99100## Notes101102- The Stop hook and this loop both claim by atomic rename, so they never103 double-process a comment even if they run close together.104- Both paths honor a request's `sessionId`, so another Claude session cannot105 steal feedback intended for the article's authoring session.106- Keep replies short and factual — they surface back in the author's HUD next107 to their original comment.