$npx -y skills add enjalot/moonshine --skill moonshine-listenListen 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 se
| 1 | # moonshine-listen — authorship feedback listener (Claude Code adapter) |
| 2 | |
| 3 | This is the **idle-coverage half** of the moonshine authorship-feedback loop |
| 4 | (`plugins/moonshine/FEEDBACK.md`). The Stop hook already handles delivery at |
| 5 | turn boundaries; this skill covers requests that land while the session is |
| 6 | idle, and powers the HUD's live status (the heartbeat) and its mode controls |
| 7 | (the `control.json` it reads). Note the default mode is **accumulate**: |
| 8 | comments pile up and are only drained when the author presses Address in the |
| 9 | HUD (`address.json`) or has opted into continuous auto-address (`listen`). |
| 10 | |
| 11 | Each invocation performs **one tick**. Run it under `/loop` so the ticks repeat. |
| 12 | |
| 13 | ## Scope |
| 14 | |
| 15 | - If invoked with a project name argument, operate on |
| 16 | `~/.agent/moonshine/<arg>/.feedback/` only. |
| 17 | - Otherwise operate on every existing `~/.agent/moonshine/*/.feedback/` inbox. |
| 18 | - If `MOONSHINE_FEEDBACK=off` is set in the environment, do nothing and end |
| 19 | the loop — the same kill switch the Stop hook and the dev server honor. |
| 20 | |
| 21 | Use absolute, ISO-8601 UTC timestamps (`date -u +%Y-%m-%dT%H:%M:%SZ`) everywhere. |
| 22 | |
| 23 | ## One tick |
| 24 | |
| 25 | For each in-scope `.feedback/` directory: |
| 26 | |
| 27 | 1. **Manifest.** If `adapter.json` is missing, write |
| 28 | `{"harness":"claude-code","version":"adapter","installedAt":"<now>"}`. |
| 29 | |
| 30 | 2. **Read control.** Read `control.json`; treat a missing file as |
| 31 | `{"mode":"accumulate"}` (the default: hold comments, don't drain). |
| 32 | - If `mode == "stopped"` and there is no `address.json`: write a final `heartbeat.json` with |
| 33 | `"mode":"stopped"` and the current `ts`, then **end** — do not schedule |
| 34 | another tick for this project. (If all in-scope projects are stopped, end |
| 35 | the loop entirely; tell the user the listener stopped.) |
| 36 | |
| 37 | 3. **Route to the authoring session.** Read the optional `sessionId` from |
| 38 | `address.json` (for a one-shot request), or from `control.json` when mode is |
| 39 | `listen`. Compare it with `$CLAUDE_CODE_SESSION_ID`. If the request names a |
| 40 | different session — or names one but the current session ID is unavailable |
| 41 | — skip this project entirely and do not consume its request. Legacy files |
| 42 | without a `sessionId` remain unscoped. |
| 43 | |
| 44 | 4. **Heartbeat.** Write `heartbeat.json`: |
| 45 | ```json |
| 46 | {"harness":"claude-code","mode":"<mode>","ts":"<now>","intervalSec":90,"pending":<n>} |
| 47 | ``` |
| 48 | where `<n>` is the number of `status:"pending"` comment files. |
| 49 | |
| 50 | 5. **Drain (only when the author asked).** Drain when `mode == "listen"` |
| 51 | (continuous auto-address), **or** whenever `address.json` exists (the HUD's |
| 52 | explicit one-shot Address request). Address is a deliberate override of |
| 53 | pause/stop: drain once while leaving the underlying control mode unchanged. |
| 54 | After a drain pass |
| 55 | triggered by `address.json`, delete `address.json` — the request is |
| 56 | consumed. When `mode == "accumulate"` and there is no `address.json`, skip |
| 57 | draining entirely: comments accumulate by design. |
| 58 | For each comment file (any `*.json` other than `control.json` / |
| 59 | `heartbeat.json` / `adapter.json` / `address.json`) that |
| 60 | is either `status == "pending"` **or** `status == "delivered"` with a |
| 61 | `deliveredAt` more than 300s ago (a comment claimed by an earlier |
| 62 | turn/tick that was never addressed — re-surface it rather than strand it): |
| 63 | - Claim it exclusively by first renaming the record itself to a private name |
| 64 | (`mv <id>.json <id>.json.claiming.$$`). That rename is the mutual-exclusion |
| 65 | point — if another drainer already took it, your `mv` fails and you skip |
| 66 | the comment. Renaming a rewritten copy *over* the original is NOT a claim |
| 67 | (both racers would win). Then rewrite the claimed file with |
| 68 | `status:"delivered"` and `deliveredAt:"<now>"` and rename it back to |
| 69 | `<id>.json`. |
| 70 | - Read `target` (`path`, `kind`, `figureId`, `range`, `excerpt`, `anchorHash`) |
| 71 | and `comment`. The source file is `<project>/content/<target.path>`. |
| 72 | - If `anchorHash` no longer matches the current file body, the prose moved — |
| 73 | locate the passage by `excerpt` instead of trusting `range`. |
| 74 | - **Address the comment** by editing the source file (or the figure |
| 75 | component / registry it points at). |
| 76 | - Record the outcome: set `status:"addressed"`, `addressedAt:"<now>"`, and a |
| 77 | one-line `reply` summarizing what you changed. Write atomically. |
| 78 | - When `mode == "paused"` or `"stopped"` and there is no address request, |
| 79 | skip draining. If stopped with an address request, drain once, write the |
| 80 | final stopped heartbeat, and then end this project's loop. |
| 81 | |
| 82 | ## Continue the loop |
| 83 | |
| 84 | After the tick, unless every in-scope project was `stopped`: |
| 85 | - When running under `/loop` (dynamic): call **Sche |