# Working in this repo ## Start here - `plans/` holds the build plan and full spec (local only, git-ignored). Read `plans/README.md` first. - `pnpm check` must pass before every commit: Biome lint, `tsc -b`, Vitest. Engine and end-to-end tests skip on hosts without FFmpeg and MLT; `docker build --target test .` runs everything (it sets `CUTTINGFLOOR_REQUIRE_ENGINES=1`, so a missing engine fails instead of skipping). - Docker is the product's only runtime requirement. Engines and tools ship inside the image; never add a host dependency. ## Conventions - TypeScript run directly by Node 24 (type stripping). Only erasable syntax: no enums, namespaces or parameter properties. Relative imports carry `.ts`. - Workspace packages export `./src/index.ts`; there is no build step. The Docker image runs the source directly. - Zod v4 (`import * as z from 'zod/v4'`) for every schema. Tool input and output schemas live in `packages/schema`. - MCP via the v2 TypeScript SDK (`@modelcontextprotocol/server`). Spec target: MCP 2026-07-28. HTTP goes through `createMcpHandler` (modern plus stateless 2025 fallback). - Stateless: timelines are immutable and content-addressed; every durable thing is a file with a TTL under the data root. No database, no project folders, no in-memory state a restart would lose. - Engines are subprocesses (`packages/engine`), always called with argument lists, never a shell. Only the server writes MLT XML, with forced producers. - Tool names are `domain.verb`. Tools are granular editing primitives; anything that is a composition of primitives, data or derivable is not a tool. No tool involves a human. - Core tenet: tools give the agent actions (edits) and facts it cannot get itself (a transcript, speech spans, frames, loudness); judgment and composition are the agent's. A composition that isn't obvious goes in a skill (`skills/`), a soft harness of steps using the tools. It becomes a script or a tool only on evidence: it is common, agents measurably struggle with the skill, and the tool brings something the agent cannot reproduce. A tool never merges sources or decides behind the agent's back. Test: a tool is a verb (cut, add, trim, measure); a result such as loudness is a fact a tool returns, not a tool. No business numbers in tools: thresholds, targets, grouping rules and verdicts are the agent's arguments or don't exist. Limits that protect the server (sizes, durations, timeouts, batch caps) stay. Facts follow the same rule: don't pick facts to fit one incident. Return data the server already reads in full (all of a probe, not chosen fields), keep previews honest (transparency over a checkerboard, true file formats) so the agent can see for itself, and add a measured fact only when the agent can't perceive or compute it (speech edges, sound events, sync offset). Skills are rare: only a method a capable agent would not work out from the tools and their descriptions. This holds for fixes to existing tools too. - Tools are agnostic: never name or shape a tool after the thing it acts on or the job it serves. "Remove the logo", "blur the dog" and "brighten the mug" are one chain, as in Photoshop: select (a lasso or shape mask, or an item by its bounds), then act on the selection (show, hide, grade, blur, fill). Bounds and other inputs come from tools that already exist (`track`, `faces`, `judge.ask` boxes). A gap is the one missing verb or fact in such a chain, named for what it does and what it takes (a mask, bounds, a time range), never "X removal". Test a gap blind: a fresh agent with only the MCP tries the job; what it assembles is covered, what it can't is the gap. - Every tool declares its output schema, and results always match it (including the job a long-running tool returns). Clients chain calls (in their own code execution) before any result exists, so the output signature is the contract. cuttingfloor runs no client code: running MCP servers from code is the client's job. - The deliberate exception is `skills.*` (`skills.search`, `skills.get`): not editing, but discovery of how to compose the primitives for a common job. A composition is a skill, `skills/.md` in one-mcp's playbook format (checked by `packages/skills`), never a tool. - Domain errors are `CfError` (typed code, actionable hint) and reach clients as `isError` results. - No provider key ever appears in a tool argument, result, error, log or stored file. A key the user saves on their account page (their sync. labs key) is kept only as ciphertext (AES-256-GCM, bound to the user, with a server-only secret) in their WorkOS metadata, never on the server's disk, and only its last four characters are shown. - Tests use the SDK `Client` against `createServer` (InMemoryTransport) or the HTTP server, not internal functions. ## Branches and releases Open source: outside contributors fork, branch from `develop` and open pull requests into `develop` (CONTRIBUTING.md). Agents follow the same flow from worktrees: - The main checkout (first in `git worktree list`) is the person's and stays on `develop`. Never edit, commit or switch branches there. - Start: a worktree off `origin/develop` on a branch prefixed with the harness (`claude/…`, `cursor/…`, `codex/…`). Claude Code worktrees live in `.claude/worktrees/`; run `scripts/worktree-setup.sh` in a new one (env files and its own `pnpm install`). - Ship: `pnpm check` in the worktree, then `git push origin HEAD:develop` (rebase on `origin/develop` if rejected), or a pull request into `develop`. - Promote: `scripts/promote-develop.sh ""` opens the develop → main pull request, waits for its required checks (not the image build, which gates the server deploy on `main` and skips site-only changes) and merges it with a merge commit (never squash or rebase, which would leave `develop` diverged from `main`). It versions pending changesets on `develop` first; on `main` the release workflow tags the version and publishes the image (it commits nothing). Then it brings `main` back into `develop` (`scripts/merge-main-into-develop.sh`: a fast-forward to the promotion, or a merge commit when `develop` has moved; never a rebase or force), so `develop` is never behind `main`. `main` takes nothing but that pull request. Only `develop` may open a pull request into `main` (the Flow check). - Quick: a change to words only (tool descriptions, hints, titles, docs) can ship with `[quick]` in the first line of its commit message and `promote-develop.sh "" --quick`; the image run then skips the engine suite (about 10 minutes), while `pnpm check`, the smoke test and the builds still gate the deploy. Anything that changes behaviour runs the full suite. - Deploys are automatic, never by hand: merging into `main` deploys the site (Vercel builds `main`; `develop` gets a preview) and the hosted server (`Image` publishes, then `Deploy` pulls it on the Hetzner server and checks `/mcp`). The `ship` skill (`.claude/skills/ship`) is the checklist. - Finish: `scripts/finish-release.sh ` waits for main's runs, brings `main` into `develop` again if anything reached `main` since, removes the worktree and fast-forwards the main checkout. - Whether something exists is answered from `origin/develop` and `origin/main` (`git grep -n origin/develop --`), not a working tree that may be behind. ## Commits Conventional, small, one concern each. Add a changeset (`pnpm changeset`) for user-facing changes; only the `cuttingfloor` app is versioned, and releases tag the image.