# CLAUDE.md Guidance for Claude Code (and the crew) working in this repo. ## What this is **Slate: the Vivijure Screenwriter's Assistant for Discord.** A collaborative film-planning bot that maintains a storyboard brief, generates character portraits and scene thumbnails, searches the web, and submits projects to the Vivijure render pipeline (full studio API parity + control-panel conformance). Version: see root **`package.json`** / latest `v*` tag / `CHANGELOG.md` (do not treat a number in this file as current forever). The GitHub repo is named `slate` (it was `skyphusion-slate`; redirects still work). Production runs as a Docker stack on the deploy host. **Not hostable multi-tenant.** Slate is a single-operator Discord bot pointed at one studio `VIVIJURE_API_URL`. It is not the control plane and not a tenancy product. **Full command reference:** [docs/commands.md](docs/commands.md) ## Where it sits (the Vivijure constellation) ``` friends + Discord <-- THIS REPO (slate) | v vivijure-cf / vivijure-local (studio panels; core: vivijure-core) | v vivijure-backend + satellites (GPU render) ``` Slate is the upstream, human-facing surface. The group writes a film in Discord; Slate keeps the brief and ships it to the Vivijure studio API. Slate has **full control-panel parity**: every hook, cast workflow, render setting, and HTTP route the web UI exposes is reachable from Discord. Module choices are projected live from `GET /api/modules` via `registry.mjs`; commands are gated on what is actually installed. ## Structure ``` bot.mjs Discord bot (main entry, message + slash handlers) lib.mjs Pure helpers (brief transforms, registry mapping, preflight formatting) registry.mjs GET /api/modules projection (hooks, gates, formatters) studio.mjs HTTP client for all studio API routes (69: 68 CONTRACT + retry) contract.mjs Route-to-command matrix; STUDIO_COMMAND_ALIASES; CI validation studio-api.mjs !api action registry (65 actions) + executeStudioAction() contract.test.ts Vitest: zero-drift conformance gate lib.test.ts Vitest unit tests for lib.mjs registry.test.ts Vitest unit tests for registry.mjs search-worker/ Cloudflare Worker slate-search: web search + knowledge base log-worker/ Cloudflare Worker slate-logs: log sink stacks/compose.prod.yml Docker Compose stack (production) docs/commands.md Canonical command + studio parity reference docs/CONTRACT-conformance.md 69-route API matrix (CI-enforced) ``` ## Commands (developer) ```bash npm run lint # node --check bot.mjs -- CI gate for the bot npm run bot # node bot.mjs (process env only; ./run.sh sources slate.env) npm test # vitest: lib + registry + contract conformance npm run smoke:studio # offline gate + optional live studio probes npx vitest run # same as npm test cd search-worker && npm run typecheck && npm run deploy cd log-worker && npm run typecheck && npm run deploy ``` ### Verifying changes `node --check bot.mjs` (`npm run lint`) is the bot gate. Workers typecheck separately. `npm test` (`vitest run`) covers `lib.test.ts`, `registry.test.ts`, `contract.test.ts` (69-route zero-drift gate), and `bot.test.ts` (boot smoke against mocked env), plus the other root `*.test.ts` files. CI: `ci.yml` lints the bot, runs that Vitest suite, and typechecks `search-worker` and `log-worker`. `coverage.yml` is the required `coverage` context; it currently runs no tests (passthrough echo). The Vitest gate in CI is `ci.yml`. **`deploy.yml` deploys Workers only on a pushed `v*` tag**, not on a bare merge to `main` (main runs CI only). Bot image builds on `v*.*.*` tags (`image.yml`). ## Running (production) Bot runs as Docker on the stack host (tag-driven GHCR image). Workers deploy from CI **only on a `v*` tag** (see Release / tagging). See README "Run your own Slate" and `stacks/compose.prod.yml`. **Compose pin discipline:** `stacks/compose.prod.yml` must pin the bot image to an explicit version tag matching the cut release (e.g. `ghcr.io/skyphusion-labs/slate:X.Y.Z`), not a floating dig that drifts from `package.json`. When cutting a release, bump package + CHANGELOG **and** the compose pin in the same wave (or immediately after the GHCR publish). Never leave compose on `:latest` as the production contract. ## Key architecture - **Chat model:** `bot.mjs` defaults `DISCORD_MODEL` to `qwen3.6:27b-ctx8k`. With `CF_AIG_TOKEN` unset, chat uses ollama at that id. With `CF_AIG_TOKEN` set, that same id is sent to Claude through the CF AI Gateway (native Anthropic SDK), so the Claude path needs a Claude model id. `stacks/compose.prod.yml` and `slate.env.example` default `DISCORD_MODEL` to `claude-sonnet-4-6`. - **Tool-use loop** (5 rounds max): web_search, research, fetch_page, search_knowledge, search_memory via slate-search Worker. - **Vision**: image attachments as base64 to Claude (ollama path strips to text). - **D1 session state**: brief, history, briefHistory, render_settings, cast_bindings, studio_project_id, render_jobs, traffic_ledger. - **Traffic ledger + session-memory RAG (slate#90):** every studio.mjs call flows through `studioRequest`, which fires an observer (`setStudioRequestObserver`, wired in bot.mjs) that logs the full request/response to D1 `traffic_ledger` and, for mutating calls, embeds a summary into the `slate-memory` Vectorize index (search-worker `/memory/index`). Chat turns and brief snapshots are embedded the same way, on save and on `extractBrief`. `channelContext` (an `AsyncLocalStorage` entered at the top of the `MessageCreate` / `InteractionCreate` handlers) attributes every write to the right Discord channel without threading a channelId through every studio.mjs call site. Retrieval: `search_memory` tool (automatic, mid-conversation) and `!memory` / `/memory` (manual). `slate-memory` is a separate Vectorize index from `slate-knowledge` (the manual `!learn` corpus) so per-channel session context never leaks across channels or mixes into the shared knowledge base. - **Registry projection**: `registry.mjs` mirrors planner-registry.js + planner-render-config.js. Hook catalog, pick_one choosers, module config, and command gates all from `GET /api/modules`. - **Studio client**: `studio.mjs` implements every route in the conformance matrix (`contract.mjs`). `studio-api.mjs` maps 65 friendly action names for `!api`. `bot.mjs` adds ergonomic `!` commands, `STUDIO_COMMAND_ALIASES`, and attachment upload flows. `!conformance` prints the live matrix. - **Render submit**: bundle -> preflight -> `POST /api/render/film` with mapped configs; polls every 30s. - **Module-gated commands**: `commandAvailability()` in registry.mjs; `!commands` lists live gates. ## Discord commands (summary) See [docs/commands.md](docs/commands.md) for the full table. Highlights: | Area | Commands | |------|----------| | Storyboard | `!brief`, `!undo`, `!reset`, `!autodirect` | | Render settings | `!tier`, `!keyframe`, `!backend`, `!keyframes-only`, `!titlecard`, `!subtitles`, `!config`, `!install-config`, `!hooks` | | Submit | `!render`, `!ship`, `!preflight` | | Cast / LoRA | `!cast`, `!bind`, `!train`, `!genrefs`, `!voices`, `!voice` | | Uploads | `!importcast`, `!upload`, `!audioupload`, `!addref`, `!addsource`, `!characterref` | | Studio | `!saveproject`, `!loadproject`, `!renders`, `!score`, `!api`, `!conformance` | | Meta | `!commands` (module-gated live list) | Every command has a slash equivalent where practical. Slash commands register globally on startup. ## Conventions - **No em-dashes (U+2014) or en-dashes (U+2013)** in source, comments, or docs. - **Handle / username is `skyphusion`** across services. - **Minimal dependencies**: vanilla Node.js + discord.js + Anthropic SDK. - **Secrets never committed**; config via environment variables. - **Trust the studio registry** over hardcoded module names, tiers, or hook lists. - **CSAM bright-line** absolute. **Ignore Cursor `AGENTS.md`.** No endpoint-ID freezes. - **Typecheck** Workers with `npm run typecheck` in `search-worker/` / `log-worker/` before push; bot gate is `npm run lint`. ## Crew + identity Crew members work as their own Unix + gh identity (`sudo -n -H -u bash -lc '...'`). Crew commits use `skyphusion-` identity, never Conrad's. Conrad devs only on his laptop (`Conrad Rockenhaus `). ## Commits & versioning Conventional Commits; SemVer `0.MINOR.PATCH` pre-1.0. Release bumps root `package.json` + `CHANGELOG.md`. ## Release / tagging Three ship paths share the same SemVer cut (`package.json` + `CHANGELOG.md` `## vX.Y.Z` on `main`): | Trigger | Workflow | Effect | |---------|----------|--------| | push `v*` tag | `deploy.yml` | Deploy slate-search + slate-logs Workers | | push `v*.*.*` tag | `image.yml` | Build/push bot GHCR image | | **GitHub Release published** on that tag | `publish-npm.yml` | Publish `@skyphusion/slate` to npm | Merge to `main` alone does **not** deploy Workers, push the image, or publish npm. ### Cut a release 1. **Release PR on `main`:** bump root `package.json` version, add `CHANGELOG.md` `## vX.Y.Z`, land PR. 2. **Tag** (must match `package.json`): ```bash git fetch origin main && git checkout main && git pull --ff-only git tag -a vX.Y.Z -m "vX.Y.Z" git push origin vX.Y.Z ``` 3. Confirm `deploy.yml` / `image.yml` green. 4. **npm** (fires `publish-npm.yml`): ```bash gh release create vX.Y.Z --title "vX.Y.Z" --generate-notes ``` Tag must be an ancestor of `origin/main`. npm version guard: release tag `vX.Y.Z` == `package.json` version `X.Y.Z`.