Ctrl K
ComunidadGuías…

CLAUDE.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# 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`.