snappy-chain skill
$ npx snappy-skills install snappy-chain
Where snappy-channel-contract gives uniform hands and eyes across every channel, snappy-chain turns sensed events into action. Cron-safe, dedup-enforced, channel-agnostic handlers.
adapters[] ─read─▶ Event[] ─classify─▶ Intent ─route─▶ ChainHandler ─post─▶
│
▼
chain-processed.ndjson (dedup)
typescriptimport { dispatchOnce, registerChain } from "../snappy-chain/api.ts";
registerChain("bug-report", async (event, adapter) => {
// ... clone repo, fix, screenshot, open PR ...
const post = await adapter.post(
{ source: event.source, channel_id: event.channel_id, thread_id: event.thread_id, to_user: null },
{ text: "Fixed in PR #123" },
);
return { ok: true, intent: "bug-report", chain: "bug-report", event_id: event.event_id, source: event.source, post };
});
const summary = await dispatchOnce({ limit: 20 });
bash# dry run — classify + log, do not fire handlers
npx tsx ~/.claude/skills/snappy-chain/api.ts --dry
# classifier self-test
npx tsx ~/.claude/skills/snappy-chain/classify.ts
~/.claude/logs/chain-processed.ndjson — one line per (source, event_id) ever seen. Dedup source of truth.~/.claude/logs/chain-runs.ndjson — one line per dispatchOnce() run with counts + results.(event, adapter), use contract verbs. No hard-coded Slack calls inside a handler.adapter.post(). Per-channel rules (e.g. Gmail draft-only) stay honored.bug-report — error/broken/crash languagequestion — ends with ? or starts with how|what|why|wherefeature-request — "can you add...", "would be nice if..."ack — "thanks", "got it", "perfect"noise — everything else (still marked processed to stop re-classification)V1 is pure keyword rules. Upgrade path: LLM fallback via snappy-dispatch for unmatched events.
chains/<intent>.ts exporting a ChainHandler.registerChain("<intent>", handler) once at startup.Intent in classify.ts and add rules.<!-- SKILL-INDEX-START -->
[snappy-chain Index]|root: ~/.claude/skills/snappy-chain|IMPORTANT: Prefer these files over pre-training assumptions for this domain. Read the relevant file when the AGENTS.md summary is insufficient.|root:{SKILL.md}
<!-- SKILL-INDEX-END -->
Nothing in the collection names this skill.
<!-- SNAPPY-CONTRACT-VERBS-START -->
Generated from api.ts HAND_CONTRACT. Do not hand-edit this block.
This skill declares no executable verbs. Its instruction-only label is intentional.
When an answer carries face_hint, show it with one snappy_present(<answer>) call.
See /snappy-faces for face selection. Human-facing images must crop to the
element, render at 2x on Retina, and fill the destination channel instead of
placing a small card in a full-page screenshot.
<!-- SNAPPY-CONTRACT-VERBS-END -->
---
name: snappy-chain
role: Event-to-chain dispatcher sitting on top of snappy-channel-contract
loaded-by: preload-skill-context hook
---
# snappy-chain — The Composer Layer
Where `snappy-channel-contract` gives uniform hands and eyes across every channel, `snappy-chain` turns sensed events into action. Cron-safe, dedup-enforced, channel-agnostic handlers.
## The loop
```
adapters[] ─read─▶ Event[] ─classify─▶ Intent ─route─▶ ChainHandler ─post─▶
│
▼
chain-processed.ndjson (dedup)
```
## API
```typescript
import { dispatchOnce, registerChain } from "../snappy-chain/api.ts";
registerChain("bug-report", async (event, adapter) => {
// ... clone repo, fix, screenshot, open PR ...
const post = await adapter.post(
{ source: event.source, channel_id: event.channel_id, thread_id: event.thread_id, to_user: null },
{ text: "Fixed in PR #123" },
);
return { ok: true, intent: "bug-report", chain: "bug-report", event_id: event.event_id, source: event.source, post };
});
const summary = await dispatchOnce({ limit: 20 });
```
## Commands
```bash
# dry run — classify + log, do not fire handlers
npx tsx ~/.claude/skills/snappy-chain/api.ts --dry
# classifier self-test
npx tsx ~/.claude/skills/snappy-chain/classify.ts
```
## Logs
- `~/.claude/logs/chain-processed.ndjson` — one line per `(source, event_id)` ever seen. Dedup source of truth.
- `~/.claude/logs/chain-runs.ndjson` — one line per `dispatchOnce()` run with counts + results.
## Rules
1. **Channel-agnostic handlers only.** Receive `(event, adapter)`, use contract verbs. No hard-coded Slack calls inside a handler.
2. **Publish through `adapter.post()`.** Per-channel rules (e.g. Gmail draft-only) stay honored.
3. **Every run logs a summary.** Run log is the chain-level cert.
4. **Failed chains don't auto-retry.** Root-cause, fix, manually re-inject.
5. **Dedup is mandatory and automatic.** Dispatcher owns it, not handlers.
## Intents (V1 classifier)
- `bug-report` — error/broken/crash language
- `question` — ends with `?` or starts with `how|what|why|where`
- `feature-request` — "can you add...", "would be nice if..."
- `ack` — "thanks", "got it", "perfect"
- `noise` — everything else (still marked processed to stop re-classification)
V1 is pure keyword rules. Upgrade path: LLM fallback via `snappy-dispatch` for unmatched events.
## When to add a new chain
1. Create `chains/<intent>.ts` exporting a `ChainHandler`.
2. Call `registerChain("<intent>", handler)` once at startup.
3. Dry-run the dispatcher against recent sweep data to verify classification fires.
4. If the intent doesn't exist yet, add it to `Intent` in `classify.ts` and add rules.
<!-- SKILL-INDEX-START -->
[snappy-chain Index]|root: ~/.claude/skills/snappy-chain|IMPORTANT: Prefer these files over pre-training assumptions for this domain. Read the relevant file when the AGENTS.md summary is insufficient.|root:{SKILL.md}
<!-- SKILL-INDEX-END -->
## Used by
Nothing in the collection names this skill.
<!-- SNAPPY-CONTRACT-VERBS-START -->
## Contract verbs
Generated from `api.ts` `HAND_CONTRACT`. Do not hand-edit this block.
This skill declares no executable verbs. Its `instruction-only` label is intentional.
## Show the result
When an answer carries `face_hint`, show it with one `snappy_present(<answer>)` call.
See `/snappy-faces` for face selection. Human-facing images must crop to the
element, render at 2x on Retina, and fill the destination channel instead of
placing a small card in a full-page screenshot.
<!-- SNAPPY-CONTRACT-VERBS-END -->