From moonshine
Monitors moonshine authorship feedback inboxes, processes control/address requests, and drains pending comments. Runs under /loop for idle coverage.
How this skill is triggered — by the user, by Claude, or both
Slash command
/moonshine:moonshine-listenThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
This is the **idle-coverage half** of the moonshine authorship-feedback loop
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.
~/.agent/moonshine/<arg>/.feedback/ only.~/.agent/moonshine/*/.feedback/ inbox.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.
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).
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):
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.target (path, kind, figureId, range, excerpt, anchorHash)
and comment. The source file is <project>/content/<target.path>.anchorHash no longer matches the current file body, the prose moved —
locate the passage by excerpt instead of trusting range.status:"addressed", addressedAt:"<now>", and a
one-line reply summarizing what you changed. Write atomically.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.After the tick, unless every in-scope project was stopped:
/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)./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.)sessionId, so another Claude session cannot
steal feedback intended for the article's authoring session.npx claudepluginhub enjalot/moonshine --plugin moonshineSweeps feedback sources (Slack, GitHub Issues) for new items, acknowledges them, analyzes recordings, verifies fixes, and emits a plan. Automates feedback triage.
Reviews feedback captured from Claude Code sessions, processes sessions with LLM analysis, reviews pending items, and exports to GitHub issues for Product Forge.
Handles inbound messages from Telegram, Discord, and webhook channels, routing responses back through the channel with session context awareness.