CLAUDE.md
Rama main · réplica de lectura del repositorio original.
# 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 <member> bash -lc '...'`). Crew commits
use `skyphusion-<member>` identity, never Conrad's. Conrad devs only on his laptop
(`Conrad Rockenhaus <conrad@skyphusion.org>`).
## 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`.