{"_id":"@authorofthesurf/stagecraft","_rev":"5-cbcc2405beb13054a51e87c0701ab47b","name":"@authorofthesurf/stagecraft","dist-tags":{"latest":"0.4.1"},"versions":{"0.1.0":{"name":"@authorofthesurf/stagecraft","version":"0.1.0","author":{"name":"Zane Kansil"},"license":"MIT","_id":"@authorofthesurf/stagecraft@0.1.0","maintainers":[{"name":"authorofthesurf","email":"zanekansil@gmail.com"}],"homepage":"https://github.com/AuthorOfTheSurf/stagecraft#readme","bugs":{"url":"https://github.com/AuthorOfTheSurf/stagecraft/issues"},"bin":{"stagecraft":"dist/tools/hello.js"},"dist":{"shasum":"9313a848b3f4536246b581b723a58788af5f5433","tarball":"https://registry.npmjs.org/@authorofthesurf/stagecraft/-/stagecraft-0.1.0.tgz","fileCount":43,"integrity":"sha512-sdcR57Q2gQqlu0LHkW7Q3Yc5VIRIRFdpcQL0J8yAqrqxqwIVXGGFtDaWUT3mVj7Er6xQLZ2EMk49aZibqEKKaQ==","signatures":[{"sig":"MEUCIQDXVPsBhxEsd+0bgzasm9wmowSrBQZBpve330NyFUqAGAIgKixUU+LCZ/8aZikzHxJq1VZcFzhL0rvw5KxQn35YLB0=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":144775},"type":"module","exports":{".":{"bun":"./src/index.ts","types":"./dist/index.d.ts","default":"./dist/index.js"},"./panel":{"bun":"./src/tools/panel.ts","types":"./dist/tools/panel.d.ts","default":"./dist/tools/panel.js"},"./testing":{"bun":"./src/tools/testing.ts","types":"./dist/tools/testing.d.ts","default":"./dist/tools/testing.js"}},"gitHead":"c4850ba58c5db5493cd87a9d2c86673db45b1909","scripts":{"demo":"bun examples/demo-panel.ts","test":"bun test","build":"rm -rf dist && tsc -p tsconfig.build.json","hello":"bun src/tools/hello.ts","typecheck":"tsc --noEmit","prepublishOnly":"bun run typecheck && bun test && bun run build"},"_npmUser":{"name":"authorofthesurf","email":"zanekansil@gmail.com"},"repository":{"url":"git+https://github.com/AuthorOfTheSurf/stagecraft.git","type":"git"},"_npmVersion":"11.6.1","description":"Plain actors on Rivet: async handlers, durable state, typed errors — plus the crew: error reports, issue grouping, alert adapters, and a live monitor panel.","directories":{},"sideEffects":false,"_nodeVersion":"24.10.0","dependencies":{"effect":"4.0.0-beta.66","@rivetkit/effect":"2.3.10"},"_hasShrinkwrap":false,"devDependencies":{"@types/bun":"^1.3.14","typescript":"^5.9.3"},"_npmOperationalInternal":{"tmp":"tmp/stagecraft_0.1.0_1787474892534_0.3935636568481682","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@authorofthesurf/stagecraft","version":"0.2.0","author":{"name":"Zane Kansil"},"license":"MIT","_id":"@authorofthesurf/stagecraft@0.2.0","maintainers":[{"name":"authorofthesurf","email":"zanekansil@gmail.com"}],"homepage":"https://github.com/AuthorOfTheSurf/stagecraft#readme","bugs":{"url":"https://github.com/AuthorOfTheSurf/stagecraft/issues"},"bin":{"stagecraft":"dist/tools/hello.js"},"dist":{"shasum":"4a2de1ac320a93f308196467956919d85e9877ec","tarball":"https://registry.npmjs.org/@authorofthesurf/stagecraft/-/stagecraft-0.2.0.tgz","fileCount":43,"integrity":"sha512-zlU5dBmm/6JXm7LADX1d6AdLIE4MmoV953GS+TRSUWSOVEk8BrqAoIMJDvArGDsGBYhi1A0Vy3lp2sO1JIRbpA==","signatures":[{"sig":"MEYCIQDrvHlS4sXZSrUIABANPlfPUeLu1ySiQGS1OM9Ac3rZuQIhAPfuMZ0HZKAafoF4QfX52iZRQ0ViaZ2+qIHQjby8He3A","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":148339},"type":"module","exports":{".":{"bun":"./src/index.ts","types":"./dist/index.d.ts","default":"./dist/index.js"},"./panel":{"bun":"./src/tools/panel.ts","types":"./dist/tools/panel.d.ts","default":"./dist/tools/panel.js"},"./testing":{"bun":"./src/tools/testing.ts","types":"./dist/tools/testing.d.ts","default":"./dist/tools/testing.js"}},"gitHead":"ae7fbaea5c84f7d82493d0d088a69cf1dc2da7fd","scripts":{"demo":"bun examples/demo-panel.ts","test":"bun test","build":"rm -rf dist && tsc -p tsconfig.build.json","hello":"bun src/tools/hello.ts","typecheck":"tsc --noEmit","prepublishOnly":"bun run typecheck && bun test && bun run build"},"_npmUser":{"name":"authorofthesurf","email":"zanekansil@gmail.com"},"repository":{"url":"git+https://github.com/AuthorOfTheSurf/stagecraft.git","type":"git"},"_npmVersion":"11.6.1","description":"Plain actors on Rivet: async handlers, durable state, typed errors — plus the crew: error reports, issue grouping, alert adapters, and a live monitor panel.","directories":{},"sideEffects":false,"_nodeVersion":"24.10.0","dependencies":{"effect":"4.0.0-beta.66","@rivetkit/effect":"2.3.10"},"_hasShrinkwrap":false,"devDependencies":{"@types/bun":"^1.3.14","typescript":"^5.9.3"},"_npmOperationalInternal":{"tmp":"tmp/stagecraft_0.2.0_1787477697865_0.9427803697316608","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@authorofthesurf/stagecraft","version":"0.3.0","author":{"name":"Zane Kansil"},"license":"MIT","_id":"@authorofthesurf/stagecraft@0.3.0","maintainers":[{"name":"authorofthesurf","email":"zanekansil@gmail.com"}],"homepage":"https://github.com/AuthorOfTheSurf/stagecraft#readme","bugs":{"url":"https://github.com/AuthorOfTheSurf/stagecraft/issues"},"bin":{"stagecraft":"dist/tools/hello.js"},"dist":{"shasum":"c898b355da9760ad489a3a3cfab039c28027c20d","tarball":"https://registry.npmjs.org/@authorofthesurf/stagecraft/-/stagecraft-0.3.0.tgz","fileCount":43,"integrity":"sha512-EyTclQoiW292XeBLvjivH1DjwpiqH99lNAQmZpLg1gisbY4b7aIajbmTvoDmB4oPkSb1RThyxfhgAEtPF7B6cw==","signatures":[{"sig":"MEYCIQDCJW41mrHTR7f2TX9VJxy360JG9vsMItbu4IUtnTceSwIhAKGlnlg06HwF0Q+Xcp38bJL8MRJCQexBSP+NSna5on7Q","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":159671},"type":"module","exports":{".":{"bun":"./src/index.ts","types":"./dist/index.d.ts","default":"./dist/index.js"},"./panel":{"bun":"./src/tools/panel.ts","types":"./dist/tools/panel.d.ts","default":"./dist/tools/panel.js"},"./testing":{"bun":"./src/tools/testing.ts","types":"./dist/tools/testing.d.ts","default":"./dist/tools/testing.js"}},"gitHead":"72f8736a1dafec06335e4d6d764053bd213f817e","scripts":{"demo":"bun examples/demo-panel.ts","test":"bun test","build":"rm -rf dist && tsc -p tsconfig.build.json","hello":"bun src/tools/hello.ts","typecheck":"tsc --noEmit","prepublishOnly":"bun run typecheck && bun test && bun run build"},"_npmUser":{"name":"authorofthesurf","email":"zanekansil@gmail.com"},"repository":{"url":"git+https://github.com/AuthorOfTheSurf/stagecraft.git","type":"git"},"_npmVersion":"11.6.1","description":"Plain actors on Rivet: async handlers, durable state, typed errors — plus the crew: error reports, issue grouping, alert adapters, and a live monitor panel.","directories":{},"sideEffects":false,"_nodeVersion":"24.10.0","dependencies":{"effect":"4.0.0-beta.66","@rivetkit/effect":"2.3.10"},"_hasShrinkwrap":false,"devDependencies":{"@types/bun":"^1.3.14","typescript":"^5.9.3"},"_npmOperationalInternal":{"tmp":"tmp/stagecraft_0.3.0_1787482055807_0.6342200074022706","host":"s3://npm-registry-packages-npm-production"}},"0.4.0":{"name":"@authorofthesurf/stagecraft","version":"0.4.0","author":{"name":"Zane Kansil"},"license":"MIT","_id":"@authorofthesurf/stagecraft@0.4.0","maintainers":[{"name":"authorofthesurf","email":"zanekansil@gmail.com"}],"homepage":"https://github.com/AuthorOfTheSurf/stagecraft#readme","bugs":{"url":"https://github.com/AuthorOfTheSurf/stagecraft/issues"},"bin":{"stagecraft":"dist/tools/hello.js"},"dist":{"shasum":"93bb10c34168fc19159073f3b46e9dd26c83cf18","tarball":"https://registry.npmjs.org/@authorofthesurf/stagecraft/-/stagecraft-0.4.0.tgz","fileCount":43,"integrity":"sha512-WzQMF3aOv0L3VTrrlWjL2z+lQdwGxro65sHLsyPNSoqLskVew6cl4BcYLGxZCGsWJfmSxAH2htUmmVSW22Wm/A==","signatures":[{"sig":"MEYCIQCgcH9BCgUjypurjv00D9Y7C2iZmDUPECxabyANBI233wIhAPJ92dXDckZKo/7LIPWM26C//VU4x+kWWagVAIm3M1Qk","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":184346},"type":"module","exports":{".":{"bun":"./src/index.ts","types":"./dist/index.d.ts","default":"./dist/index.js"},"./panel":{"bun":"./src/tools/panel.ts","types":"./dist/tools/panel.d.ts","default":"./dist/tools/panel.js"},"./testing":{"bun":"./src/tools/testing.ts","types":"./dist/tools/testing.d.ts","default":"./dist/tools/testing.js"}},"gitHead":"7297c98141105cc24d757aeb54449154761d49a5","scripts":{"demo":"bun examples/demo-panel.ts","lint":"eslint .","test":"bun test","build":"rm -rf dist && tsc -p tsconfig.build.json","hello":"bun src/tools/hello.ts","format":"prettier --write .","typecheck":"tsc --noEmit","format:check":"prettier --check .","prepublishOnly":"bun run lint && bun run format:check && bun run typecheck && bun test && bun run build"},"_npmUser":{"name":"authorofthesurf","email":"zanekansil@gmail.com"},"repository":{"url":"git+https://github.com/AuthorOfTheSurf/stagecraft.git","type":"git"},"_npmVersion":"11.6.1","description":"Plain actors on Rivet: async handlers, durable state, typed errors — plus the crew: error reports, issue grouping, alert adapters, and a live monitor panel.","directories":{},"sideEffects":false,"_nodeVersion":"24.10.0","dependencies":{"effect":"4.0.0-beta.66","@rivetkit/effect":"2.3.10"},"_hasShrinkwrap":false,"devDependencies":{"eslint":"^10.9.0","prettier":"^3.9.6","@types/bun":"^1.3.14","typescript":"^5.9.3","typescript-eslint":"^8.67.0"},"_npmOperationalInternal":{"tmp":"tmp/stagecraft_0.4.0_1787563850967_0.8396607221054693","host":"s3://npm-registry-packages-npm-production"}},"0.4.1":{"name":"@authorofthesurf/stagecraft","version":"0.4.1","description":"Plain actors on Rivet: async handlers, durable state, typed errors — plus the crew: error reports, issue grouping, alert adapters, and a live monitor panel.","license":"MIT","author":{"name":"Zane Kansil"},"repository":{"type":"git","url":"git+https://github.com/AuthorOfTheSurf/stagecraft.git"},"type":"module","sideEffects":false,"exports":{".":{"types":"./dist/index.d.ts","bun":"./src/index.ts","default":"./dist/index.js"},"./panel":{"types":"./dist/tools/panel.d.ts","bun":"./src/tools/panel.ts","default":"./dist/tools/panel.js"},"./testing":{"types":"./dist/tools/testing.d.ts","bun":"./src/tools/testing.ts","default":"./dist/tools/testing.js"}},"bin":{"stagecraft":"dist/tools/hello.js"},"scripts":{"test":"bun test","demo":"bun examples/demo-panel.ts","hello":"bun src/tools/hello.ts","typecheck":"tsc --noEmit","build":"rm -rf dist && tsc -p tsconfig.build.json","prepublishOnly":"bun run lint && bun run format:check && bun run typecheck && bun test && bun run build","format":"prettier --write .","format:check":"prettier --check .","lint":"eslint ."},"dependencies":{"@rivetkit/effect":"2.3.10","effect":"4.0.0-beta.66"},"devDependencies":{"@types/bun":"^1.3.14","eslint":"^10.9.0","prettier":"^3.9.6","typescript":"^5.9.3","typescript-eslint":"^8.67.0"},"gitHead":"e954e490283e71386122121403301525b2d847b5","_id":"@authorofthesurf/stagecraft@0.4.1","bugs":{"url":"https://github.com/AuthorOfTheSurf/stagecraft/issues"},"homepage":"https://github.com/AuthorOfTheSurf/stagecraft#readme","_nodeVersion":"24.10.0","_npmVersion":"11.6.1","dist":{"integrity":"sha512-Ieo+mEEUPi3zmBCkapx/SPX41S2sVTb7pjG/T7Cc1b8GtyvCjTCWK39PjOO2HkMGuYWxj/eCOtmNYKZ1mk4VwQ==","shasum":"d6174a171faafb3e6b96cacb7906079a9afa66b8","tarball":"https://registry.npmjs.org/@authorofthesurf/stagecraft/-/stagecraft-0.4.1.tgz","fileCount":43,"unpackedSize":184633,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIEoNX7Z18tXscdYcCmSmdSmlR05jtoSv9M1rN7+N+nPmAiEAzdR2mebr7r4exOnyjN/uBPmfCiqWVfO0SWQoGieNxsA="}]},"_npmUser":{"name":"authorofthesurf","email":"zanekansil@gmail.com"},"directories":{},"maintainers":[{"name":"authorofthesurf","email":"zanekansil@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/stagecraft_0.4.1_1787575901802_0.2366982796232846"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-23T08:48:12.344Z","modified":"2026-08-24T12:51:42.146Z","0.1.0":"2026-08-23T08:48:12.672Z","0.2.0":"2026-08-23T09:34:58.019Z","0.3.0":"2026-08-23T10:47:35.944Z","0.4.0":"2026-08-24T09:30:51.127Z","0.4.1":"2026-08-24T12:51:41.994Z"},"bugs":{"url":"https://github.com/AuthorOfTheSurf/stagecraft/issues"},"author":{"name":"Zane Kansil"},"license":"MIT","homepage":"https://github.com/AuthorOfTheSurf/stagecraft#readme","repository":{"type":"git","url":"git+https://github.com/AuthorOfTheSurf/stagecraft.git"},"description":"Plain actors on Rivet: async handlers, durable state, typed errors — plus the crew: error reports, issue grouping, alert adapters, and a live monitor panel.","maintainers":[{"name":"authorofthesurf","email":"zanekansil@gmail.com"}],"readme":"# stagecraft\n\n[![CI](https://github.com/AuthorOfTheSurf/stagecraft/actions/workflows/ci.yml/badge.svg)](https://github.com/AuthorOfTheSurf/stagecraft/actions/workflows/ci.yml)\n[![npm](https://img.shields.io/npm/v/@authorofthesurf/stagecraft)](https://www.npmjs.com/package/@authorofthesurf/stagecraft)\n[![license](https://img.shields.io/badge/license-MIT-blue)](LICENSE)\n\n> The developer-first actor framework & observability suite for [Rivet](https://rivet.dev).\n\n**stagecraft** is an ergonomic layer over [`@rivetkit/effect`](https://www.npmjs.com/package/@rivetkit/effect): you write actors as plain async handlers and get durable state, one-message-at-a-time FIFO semantics, typed errors, durable scheduling, and events — without having to learn the underlying [Effect](https://effect.website/docs/getting-started/the-effect-type/) machinery first.\n\nIt also ships the **crew** that keeps a show running: an unexpected-error channel with agent-patchable reports, Sentry-style issue grouping with regression alerts, pluggable sinks (stdout, Discord, Slack), and a zero-dependency live monitor panel.\n\n> **Status: experimental v0.x.** Stagecraft is an independent project, not an official Rivet project. The API is still evolving, so expect breaking changes. The `@rivetkit/effect` dependency is pinned; upstream changes are pulled in deliberately.\n\n---\n\n## Example\n\n```ts\nimport { actor } from \"@authorofthesurf/stagecraft\";\n\ntype Member = { name: string; joinedAt: number };\ntype ChatMessage = { sender: string; text: string; at: number };\n\nconst Moderator = actor(\"Moderator\", {\n  state: {},\n  errors: { BannedWords: {} as { reason: string } },\n  handle: {\n    Review: async ({ text }: { text: string }, { fail }) => {\n      if (text.includes(\"spam\")) {\n        throw fail.BannedWords({ reason: \"no spam allowed\" });\n      }\n    },\n  },\n});\n\nexport const ChatRoom = actor(\"ChatRoom\", {\n  state: {\n    name: \"\",\n    members: [] as Member[],\n    messages: [] as ChatMessage[],\n  },\n  events: {\n    memberJoined: {} as { member: Member },\n    memberLeft: {} as { name: string },\n    newMessage: {} as ChatMessage,\n  },\n  errors: {\n    MemberNotInRoom: {} as { member: string },\n  },\n  handle: {\n    Initialize: async ({ name }: { name: string }, { state }) => {\n      if (!state.name) state.name = name;\n    },\n\n    Join: async ({ name }: { name: string }, { state, emit, schedule }) => {\n      const member = { name, joinedAt: Date.now() };\n      state.members.push(member);\n      emit.memberJoined({ member });\n      schedule.after(250).SendMessage({\n        sender: \"Admin\",\n        text: `Welcome, ${name}!`,\n      });\n      return { memberCount: state.members.length };\n    },\n\n    Leave: async ({ name }: { name: string }, { state, emit, fail }) => {\n      if (!state.members.some((m) => m.name === name)) {\n        throw fail.MemberNotInRoom({ member: name });\n      }\n      state.members = state.members.filter((m) => m.name !== name);\n      emit.memberLeft({ name });\n    },\n\n    SendMessage: async (\n      message: { sender: string; text: string },\n      { state, actors, emit, fail },\n    ) => {\n      const isAdmin = message.sender === \"Admin\";\n      if (!isAdmin && !state.members.some((m) => m.name === message.sender)) {\n        throw fail.MemberNotInRoom({ member: message.sender });\n      }\n      await actors(Moderator).getOrCreate(\"main\").Review({ text: message.text });\n      const chatMessage = { ...message, at: Date.now() };\n      state.messages.push(chatMessage);\n      emit.newMessage(chatMessage);\n    },\n\n    GetHistory: async (_: void, { state }) => state.messages,\n  },\n});\n```\n\nDefine an actor. Use its durable state to store members and messages. Describe its message handlers in ordinary async TypeScript. With stagecraft, messages sent to the same actor instance run one at a time, in FIFO order. State changes commit _only_ when a handler succeeds. This is built on [Effect](https://effect.website/docs/getting-started/the-effect-type/) and [`@rivetkit/effect`](https://www.npmjs.com/package/@rivetkit/effect), but you don't need to know Effect jargon (generators, refs, and so on) to write plain, familiar-looking TypeScript.\n\nTake a look at [`examples/`](examples/) to find use cases similar to your own:\n\n- Durable AI agent session with human-in-the-loop approval ([support-agent.ts](examples/support-agent.ts)).\n- Real-time batch importer with a crash-safe cursor ([csv-importer.ts](examples/csv-importer.ts)).\n- Per-subscriber drip campaign on durable timers ([drip-campaign.ts](examples/drip-campaign.ts)).\n\nEach example is backed by integration tests against a real engine.\n\n---\n\n## Features\n\n```text\n┌─────────────────────────────────────────────────────────────────────────────┐\n│                                STAGECRAFT                                        │\n│  The developer-first actor framework & observability suite for Rivet             │\n└─────────────────────────────────────────────────────────────────────────────┘\n          │                                                  │\n          ▼                                                  ▼\n┌───────────────────────────────┐          ┌──────────────────────────────────┐\n│   ON-STAGE: Actor Primitives    │          │   BACKSTAGE: Observability Crew    │\n│   • Plain async handlers        │          │   • Agent-patchable error reports  │\n│   • Per-instance FIFO queues    │          │   • Sentry-style issue grouping    │\n│   • Atomic state rollback       │          │   • Discord & Slack Block Kit      │\n│   • Typed cross-actor errors    │          │   • SSE panel + QUIET watchdog     │\n│   • Durable schedule.after()    │          │   • Fail-fast two-key security     │\n│   • Client emit & routing       │          │   • Webhook connectivity tester    │\n└───────────────────────────────┘          └──────────────────────────────────┘\n```\n\n### 1. The Actor Primitive (\"On Stage\")\n\n| Feature | What it does | How it works under the hood |\n|---|---|---|\n| **Plain Async Handlers** | Write standard async functions without functional boilerplate | Wrapped in an Effect fiber runtime automatically |\n| **Atomic State Drafts** | Modify `state.count += 1` directly; commits only on success | State is cloned before execution; committed via `state.update()` on resolution or discarded on throw |\n| **Per-Instance FIFO Serialization** | Eliminates race conditions and lost updates without locks | Handlers queue on a per-instance `serialize` promise chain; distinct actors run concurrently |\n| **Typed Error Channels** | Declare domain errors with payload schemas; throw with `fail.X()` | Mapped to `Schema.TaggedErrorClass` dynamically; propagates typed across actor boundaries |\n| **Durable Timers (`schedule.after`)** | Schedule delayed messages: `const timerId = await schedule.after(ms).Action(payload)` | Backed by Rivet engine's durable scheduler (`schedule.after`), surviving reboots; hands back the scheduler's timer id. The Actor will automatically wake, do the work, then go back to sleep |\n| **Timer Cancellation (`schedule.cancel`)** | Revoke a scheduled timer: `await schedule.cancel(timerId)` | Delegates to `schedule.cancel`; `false` means already fired or unknown. Keep a state guard in the handler as the backstop for a fire already in flight |\n| **Internal Handlers (`internal`)** | Scheduled-only steps clients can't call: drip sends, expiry sweeps, work loops | Never registered as wire actions; timers reach them through a dispatcher guarded by a proof in the actor's durable kv. Forgeries reject typed (`isInternalOnly`) |\n| **Real-time Client Broadcast (`emit`)** | Broadcast events to connected clients: `emit.memberJoined(...)` | Routed through `rawRivetkitContext.broadcast()` |\n| **Direct Actor-to-Actor Routing (`actors`)** | Call other actors with full autocomplete: `actors(Mod).getOrCreate(k).Review(p)` | Uses action-level Effect context to create and invoke typed client proxies |\n| **Explicit Teardown (`destroy`)** | Cleanly terminate an actor instance when work is done | Invokes `rawRivetkitContext.destroy()` |\n\n---\n\n### 2. The Observability & Resilience Crew (\"Backstage\")\n\n| Feature | What it does | Why it matters |\n|---|---|---|\n| **Slack and Discord alerting** | Push alerts straight to company channels for immediate visibility | It's critical that unexpected Actor errors fail loudly and in a way that developers can address immediately. Sentry and other connectors coming soon |\n| **Agent-Patchable Error Reports** | Captures `reportId`, actor, action, payload, committed state snapshot, error, and stack trace | Provides the exact payload and state an AI coding agent or human needs to write a regression test and fix. Middle of the night unexpected failures come with rich error information to assist developers |\n| **Sentry-Style Issue Grouping** | Groups reports by normalized fingerprint (`Referee.Play:TypeError:undefined…`) | Strips numbers and IDs from messages so a single defect never fragments into dozens of alert groups |\n| **Smart 3-Stage Alert Policy** | • **NEW**: Alerts immediately<br>• **RECURRENCE**: Increments count silently<br>• **REGRESSION**: Alerts loudly if a resolved issue recurs | Eliminates alert fatigue while ensuring regressions are treated as high-priority incidents |\n| **Composable Alert Sinks** | Pluggable sinks: `stdoutAlert`, `discordAlert` (embeds), and `slackAlert` (Block Kit) | Easily sends structured alerts into engineering chat channels with rich formatted metadata |\n| **Fail-Fast Webhook Wiring** | Validates URL structure and scheme at startup; redacts values in errors | Prevents secret leakage in error logs and stops the process from running with a dead alert channel |\n| **Webhook Connectivity Tester (`hello.ts`)** | `bun run hello --slack [--example-error]` | Allows verifying webhook plumbing and reviewing real payload styling before booting the full application |\n\n---\n\n### 3. The Live Monitor Panel (`startPanel`)\n\n> Imported from `@authorofthesurf/stagecraft/panel`. It runs on `Bun.serve`, so it lives behind its own subpath and never enters a build that cannot run it.\n\n| Component | Capabilities |\n|---|---|\n| **Zero-Dependency SSE Server** | Serves a single-page dark-themed dashboard over native `Bun.serve` and Server-Sent Events (`/events`). No React, Tailwind, or npm client dependencies. |\n| **Actors Table with `QUIET` Watchdog** | Shows real-time activity (last action, outcome, latency in ms). Flags actors as **`● QUIET`** if they stop emitting events past a threshold, surfacing wedged actors, or dead actors. See your actors in motion and notice when they are unexpectedly inert. |\n| **Interactive Issues Table** | Displays open/resolved/regression status, total recurrence counts, and an interactive **Resolve** button for when issues are believed to be addressed. |\n| **Compact Failure Feed** | Groups repeated errors under `<details>` accordions with live incrementing counts, displaying the newest report payload, state, and stack. |\n\n---\n\n### 4. Developer & Testing Ergonomics\n\n| Tool | Problem Solved |\n|---|---|\n| **One-Line Test Engine (`testEngine`)** | Boots a local `rivet-engine` instance with typed client accessors. Merges actor layers into a single `ManagedRuntime` to prevent `Registry.test` clobbering. |\n| **Zombie Engine Reaper (`reapOrphanEngines`)** — from `@authorofthesurf/stagecraft/testing` | Automatically searches for and terminates orphaned `rivet-engine` background processes on startup, preventing port 6420 collisions. |\n| **Two-Key Security Pattern** | External alerting requires both a deliberate CLI flag (`--slack`, `--discord`) and the environment variable (`SLACK_WEBHOOK_URL`, `DISCORD_WEBHOOK_URL`), catching typos early. |\n| **Publish Quality Gate** | `prepublishOnly` script automatically runs lint, format checks, typechecking, tests, and the build before packaging to npm. |\n\n---\n\n## Start small, grow in place\n\nBuild progressively, since having complete knowledge of all the message types and error types up front is not easy. Most developer code should be \"Level 0\" i.e. familiar looking TypeScript. And the developer has the ability to drop down to lower levels to reach lower level `Effect` and `@rivetkit/effect` features when necessary\n\n- **Level 0, your everyday code** — what you see above: plain async handlers, payload types on the signature, `throw fail.X()`, mutable state draft committed only on success, typed `emit` / `schedule.after(ms)` / `actors()`.\n- **Level 1, the contract** — opt into declared schemas for wire validation and a standalone client contract.\n- **Level 2, the wiring** — declare resources/services (Effect's dependency channel), typed in the handler context, swappable in tests. Still no Effect syntax.\n- **Level 3, the engine room** — drop down to raw `Effect` / `@rivetkit/effect`, full power, a supported move.\n\n---\n\n## Built-in issue panel example\n\nEven without integrating with Discord, Slack, or Sentry, you can observe your actors in motion with a tracker and the panel. This zero-configuration example sends unexpected errors to stdout and opens the panel on `localhost:4949` by default.\n\n```ts\nimport { issueTracker, alertWith, stdoutAlert } from \"@authorofthesurf/stagecraft\";\nimport { startPanel } from \"@authorofthesurf/stagecraft/panel\";\n\nconst tracker = issueTracker();\nalertWith(tracker, stdoutAlert());\nstartPanel({ tracker });\n```\n\nTo add external alerting, set and pass `DISCORD_WEBHOOK_URL` and/or `SLACK_WEBHOOK_URL` to their corresponding adapter.\n\n```ts\nimport { issueTracker, alertWith, stdoutAlert, discordAlert, slackAlert } from \"@authorofthesurf/stagecraft\";\nimport { startPanel } from \"@authorofthesurf/stagecraft/panel\";\n\nconst { DISCORD_WEBHOOK_URL, SLACK_WEBHOOK_URL } = process.env;\nconst tracker = issueTracker();\n\nalertWith(\n  tracker,\n  stdoutAlert(),\n  discordAlert({ webhookUrl: DISCORD_WEBHOOK_URL }),\n  slackAlert({ webhookUrl: SLACK_WEBHOOK_URL }),\n);\nstartPanel({ tracker });\n```\n\n---\n\n## Run the demo\n\n```sh\nbun install\nbun run demo          # boots a real engine, opens http://localhost:4949\n```\n\nA referee actor scores rock-paper-scissors rounds and carries a realistic bug: the developer handled both winners and forgot that `winnerOf` can return `\"draw\"`.\n\nWatch the issue appear, click **Resolve**, and wait a few rounds for the 🔥 regression. With no flags you get the panel and stdout alerts — a basic error monitor with zero external dependencies.\n\nExternal alerting is two-key — a flag for intent, an env var for the credential, both required:\n\n```sh\nbun run demo --discord         # requires DISCORD_WEBHOOK_URL (setup guide: docs/qa/discord-adapter.md)\nbun run demo --slack           # requires SLACK_WEBHOOK_URL (setup guide: docs/qa/slack-adapter.md)\nbun run demo --discord --slack # both output locations simultaneously\n```\n\nA flag whose env var is missing or garbled kills the demo at boot rather than running with a silently dead channel. `DEMO_TICK_MS=8000` slows the pace.\n\nTo verify a channel is correctly wired to receive alerts use our `hello` convenience script:\n\n```sh\nbun run hello --slack                   # posts \"hello, world!\" to the channel\nbun run hello --discord                 # posts \"hello, world!\" to discord\nbun run hello --slack --example-error   # posts a realistically-shaped (clearly fake) error report\nbun run hello --slack --discord         # both output locations simultaneously\n```\n\n# Tests\n\n```sh\nbun test              # the full suite, against a real local engine\n```\n\n---\n\n## Repository Map\n\n**The package** — this is all that ships to npm.\n\n| File | What it is |\n|---|---|\n| [`src/layer.ts`](src/layer.ts) | The layer itself: `actor()`, `testEngine()`, the unexpected-error and activity channels |\n| [`src/issues.ts`](src/issues.ts) | Fingerprint grouping + the new/recurrence/regression policy |\n| [`src/adapters.ts`](src/adapters.ts) | Composable sinks: per-report `watch(...)` and issue-level `alertWith(...)` — stdout, Discord, Slack |\n| [`src/tools/panel.ts`](src/tools/panel.ts) | `stagecraft/panel` — the live panel: actors + QUIET watchdog, issues + Resolve, failure feed (SSE, zero deps) |\n| [`src/tools/testing.ts`](src/tools/testing.ts) | `stagecraft/testing` — reaps orphaned `rivet-engine` processes that would poison the next run |\n| [`src/tools/hello.ts`](src/tools/hello.ts) | The `stagecraft` bin: webhook connectivity check — `npx @authorofthesurf/stagecraft hello --slack` |\n| [`src/index.ts`](src/index.ts) | The root export: the portable core. Backstage tools sit behind their own subpaths, so importing `actor` never drags a Bun-only web server into your build. |\n\n**The exhibits** — examples and tests, repo-only.\n\n| File | What it is |\n|---|---|\n| [`examples/chat.ts`](examples/chat.ts) | The chat-room exhibit — the launch-post app at level 0 |\n| [`examples/support-agent.ts`](examples/support-agent.ts) | A durable AI agent session with human-in-the-loop approval |\n| [`examples/csv-importer.ts`](examples/csv-importer.ts) | A real-time batch importer: durable cursor, live progress events |\n| [`examples/drip-campaign.ts`](examples/drip-campaign.ts) | A per-subscriber drip sequence on durable timers |\n| [`examples/monitor-demo.ts`](examples/monitor-demo.ts) | The Referee with the forgotten-draw bug |\n| [`examples/demo-panel.ts`](examples/demo-panel.ts) | The runnable demo: `bun run demo` |\n| [`test/`](test/) | Integration tests, borrowing the examples as fixtures |\n\n**The docs.**\n\n| File | What it is |\n|---|---|\n| [`AGENTS.md`](AGENTS.md) | AI coding agent reference: rules of the stage, error contracts, and self-deadlock prevention |\n| [`docs/qa/`](docs/qa/) | From-zero setup and manual QA runbooks for Discord and Slack webhook adapters |\n| [`docs/posts/`](docs/posts/) | The story, told as posts: the intro and \"The Forgotten Draw\" |\n| [`docs/design-notes.md`](docs/design-notes.md) | Design requirements, the ladder, and known v0 hazards |\n| [`docs/upstream-strengths.md`](docs/upstream-strengths.md) | What `@rivetkit/effect` gets right, and the functionality floor |\n\n---\n\n## Design commitments\n\n- **State is the durable store.** Every actor persists a JSON state document across sleep/restart; relational storage like SQLite is an opt-in.\n- **Serialization is the actor model.** The layer runs one handler at a time per instance to fulfill the one-message-at-a-time FIFO promise.\n- **Errors are part of the contract.** Declared errors cross the wire typed and are guarded client-side; undeclared errors go to the unexpected-error channel instead of being masked or left only in process output.\n- **Effect is still reachable.** Everything is real Effect underneath; the drop-down is graceful and encouraged when you need it. Otherwise, write natural-looking TypeScript and focus on business logic.\n\n## License\n\nMIT\n\n","readmeFilename":"README.md"}