@slatesvideo/shared 0.6.2 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -4,7 +4,9 @@ export { SlatesDesktopClient, type DesktopHealth } from './clients/desktop.js';
4
4
  export { SKILLS } from './skills/content.js';
5
5
  export * as operations from './operations/index.js';
6
6
  export { ALL_OPERATIONS, VIDEO_MODELS, AUDIO_MODELS, defaultContext, type Operation, type OperationContext, type OperationResult } from './operations/index.js';
7
- export { MODEL_FACTS, getModelFact, multimodalRefSummary, multimodalRefModels, SEEDANCE_TASK_INTENT_WORDS, seedanceTaskIntentWords, type ModelFact, } from './prompts/model-facts.js';
7
+ export { MODEL_FACTS, getModelFact, multimodalRefSummary, multimodalRefModels, describeRouting, SEEDANCE_TASK_INTENT_WORDS, seedanceTaskIntentWords, type ModelFact, } from './prompts/model-facts.js';
8
8
  export { MODEL_CAPABILITIES, ALL_ASPECT_RATIOS, AGENT_ROUTE_PROVIDER, getModelCapability, aspectRatiosFor, videoResolutionsFor, defaultVideoResolutionFor, durationsFor, durationValuesFor, aspectRatioUnion, videoResolutionUnion, durationBounds, checkAspectRatio, checkVideoResolution, checkDuration, describeAspectRatios, describeVideoResolutions, describeDurations, describeReferenceImageCaps, type AspectRatio, type VideoResolution, type ModelCapability, type DurationCapability, type VideoResolutionCapability, } from './prompts/model-capabilities.js';
9
+ export { buildAgentDoctrine, type AgentSurface } from './prompts/agent-doctrine.js';
10
+ export { BANNED_PROMPT_TOKENS, describeBannedTokens, findBannedTokens, bannedTokenWarning, type BannedToken, type BannedTokenScope, } from './prompts/banned-tokens.js';
9
11
  export { PROMPTING_TIPS, getPromptingTips, type PromptingTipsEntry, type PromptingTipCard, type PromptingTipsKey } from './prompts/prompting-tips.js';
10
12
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -8,6 +8,9 @@ export { ALL_OPERATIONS, VIDEO_MODELS, AUDIO_MODELS, defaultContext } from './op
8
8
  // prompt derives its MODEL ROUTING doctrine from (kind: image | video | audio,
9
9
  // default/premium/niche notes). Edit model-facts.ts, never prose copies.
10
10
  export { MODEL_FACTS, getModelFact, multimodalRefSummary, multimodalRefModels,
11
+ // THE routing renderer — the system prompt, the MCP instructions and the
12
+ // generate ops all call this one function, so routing prose exists once.
13
+ describeRouting,
11
14
  // Mirrored in slate/src/shared/pricing.ts — see the constant's own header for
12
15
  // why the mirror exists and why it must never drive a prompt rewrite.
13
16
  SEEDANCE_TASK_INTENT_WORDS, seedanceTaskIntentWords, } from './prompts/model-facts.js';
@@ -17,6 +20,29 @@ SEEDANCE_TASK_INTENT_WORDS, seedanceTaskIntentWords, } from './prompts/model-fac
17
20
  // the op surface validates against them and GENERATES its `.describe()` prose
18
21
  // from them. Never hand-type a capability fact an LLM will read.
19
22
  export { MODEL_CAPABILITIES, ALL_ASPECT_RATIOS, AGENT_ROUTE_PROVIDER, getModelCapability, aspectRatiosFor, videoResolutionsFor, defaultVideoResolutionFor, durationsFor, durationValuesFor, aspectRatioUnion, videoResolutionUnion, durationBounds, checkAspectRatio, checkVideoResolution, checkDuration, describeAspectRatios, describeVideoResolutions, describeDurations, describeReferenceImageCaps, } from './prompts/model-capabilities.js';
23
+ // 🚨 AGENT GUIDANCE SSOT — the working method, the hard rules and the guide
24
+ // index, as ONE string both surfaces consume: the desktop Studio Agent's whole
25
+ // system prompt (slate/src/main/studio-agent/context.ts) and the MCP server's
26
+ // `instructions`. Neither may author doctrine prose of its own; the guidance
27
+ // layer drifted for exactly as long as it had no single home.
28
+ //
29
+ // Exported from the ROOT barrel only, never from ./prompts — that subpath is
30
+ // bundled by the desktop RENDERER and must stay small and Node-free, and this
31
+ // module pulls in the whole embedded SKILLS record.
32
+ // Only what a CONSUMER calls. `buildSkillIndex`, `WORKING_METHOD` and
33
+ // `HARD_RULES` are used inside agent-doctrine.ts and by nothing else, so they
34
+ // stay off the public surface — an export nothing calls is an export nothing
35
+ // keeps honest, which is why `buildModelRouting` was deleted rather than left
36
+ // here "in case".
37
+ export { buildAgentDoctrine } from './prompts/agent-doctrine.js';
38
+ // Banned prompt tokens — EXTRACTED from the skills' own never-use lists and
39
+ // inlined into the generate ops' descriptions. Enforcement of "load the guide"
40
+ // that the model cannot skip, because a description is always in context.
41
+ export {
42
+ // BANNED_PROMPT_TOKENS + describeBannedTokens: the lockstep checker and the
43
+ // ops. findBannedTokens: the eval harness scorer. bannedTokenWarning: the ops.
44
+ // `bannedTokensFor` is internal to the module and stays there.
45
+ BANNED_PROMPT_TOKENS, describeBannedTokens, findBannedTokens, bannedTokenWarning, } from './prompts/banned-tokens.js';
20
46
  // Per-model prompting tips — the SSOT for the desktop "See prompting tips"
21
47
  // modals. The desktop renders these; it never hand-writes tips content.
22
48
  export { PROMPTING_TIPS, getPromptingTips } from './prompts/prompting-tips.js';
@@ -29,7 +29,7 @@ export declare const getCreditBalance: Operation<Record<string, never>>;
29
29
  export declare const listAvailableModels: Operation<{
30
30
  filter?: string;
31
31
  }>;
32
- export declare const VIDEO_MODELS: readonly ["kling-v3.0-std", "kling-v3.0-pro", "kling-v3.0-omni", "veo-3.1-fast", "veo-3.1-standard", "seedance-2", "seedance-2.5", "omni-flash", "minimax-h3", "minimax-h3-max"];
32
+ export declare const VIDEO_MODELS: readonly ["kling-v3.0-std", "kling-v3.0-pro", "kling-v3.0-omni", "veo-3.1-fast", "veo-3.1-standard", "seedance-2", "seedance-2.5", "omni-flash", "minimax-h3", "minimax-h3-max", "ltx-2-5", "ltx-2-5-pro"];
33
33
  type VideoModel = (typeof VIDEO_MODELS)[number];
34
34
  /** The exact `model` ids `slates_edit_video` accepts. Edit rows are deliberately
35
35
  * NOT in VIDEO_MODELS — they take a source clip, not frames. */
@@ -16,7 +16,12 @@ import { BlenderBridgeClient, BLENDER_SETUP_HINT, RENDER_TIMEOUT_MS } from '../c
16
16
  import { SKILLS } from '../skills/content.js';
17
17
  // Reference-capacity prose is DERIVED, never hand-typed — root CLAUDE.md:
18
18
  // "never hand-type a fact an LLM will read". These helpers read MODEL_FACTS.
19
- import { multimodalRefSummary, multimodalRefModels, seedanceTaskIntentWords } from '../prompts/model-facts.js';
19
+ import { multimodalRefSummary, multimodalRefModels, seedanceTaskIntentWords,
20
+ // 🚨 THE routing renderer. Routing prose is GENERATED here, never typed:
21
+ // slates-mcp/CLAUDE.md forbids restating it in an op description, and this
22
+ // file did it anyway for 1,282 characters that repeated MODEL_FACTS phrase
23
+ // for phrase. Edit model-facts.ts; both surfaces follow.
24
+ describeRouting, } from '../prompts/model-facts.js';
20
25
  // 🚨 MODEL CAPABILITY SSOT. Aspect ratios, video resolutions and durations are
21
26
  // VALIDATED against and DESCRIBED from `MODEL_CAPABILITIES` — this file must
22
27
  // never re-state one of those constraints as a literal enum or a sentence
@@ -27,6 +32,13 @@ import { multimodalRefSummary, multimodalRefModels, seedanceTaskIntentWords } fr
27
32
  // 1080p. A customer burned a round trip on `4:5` + Seedance: accepted here,
28
33
  // queued, credits reserved, rejected by the provider asynchronously.
29
34
  import { AGENT_ROUTE_PROVIDER, aspectRatioUnion, videoResolutionUnion, durationBounds, aspectRatiosFor, getModelCapability, defaultVideoResolutionFor, checkAspectRatio, checkVideoResolution, checkDuration, describeAspectRatios, describeVideoResolutions, describeDurations, describeReferenceImageCaps, } from '../prompts/model-capabilities.js';
35
+ // 🚨 "LOAD THE GUIDE" MADE STRUCTURAL. The never-use token lists are EXTRACTED
36
+ // from the skill files (between `@banned` markers) and inlined into the two
37
+ // generate ops' descriptions, which are always in context on both surfaces —
38
+ // no call to skip, no discretion. `bannedTokenWarning` then reports what the
39
+ // submitted prompt actually contained, in the result, without blocking it.
40
+ // Never hand-type one of these tokens here; edit the skill.
41
+ import { describeBannedTokens, bannedTokenWarning } from '../prompts/banned-tokens.js';
30
42
  export function defaultContext() {
31
43
  return {
32
44
  cloud: () => new SlatesCloudClient(),
@@ -80,6 +92,30 @@ function creditsFromDollars(dollars) {
80
92
  // Shared describe-text for the background flag on every generate_* op.
81
93
  const BACKGROUND_DESCRIBE = 'Submit and return immediately with generationId(s) instead of blocking until the file is saved. ' +
82
94
  'Poll with slates_get_generation_status. Recommended for video (1-5 min renders).';
95
+ // ── Vision QC pointers (the "quality-check with vision" rule, made structural) ──
96
+ //
97
+ // QUALITY-CHECK is a POST-condition, so it cannot be gated the way a
98
+ // pre-condition can. What it CAN have is a result the agent cannot avoid
99
+ // reading, naming the exact op. Deliberately NOT an auto-fetch: that would
100
+ // spend vision tokens on every generation whether review was wanted or not,
101
+ // and the sandbox doctrine says the tool is available, not mandatory.
102
+ //
103
+ // ⚠️ These three differ because what the agent already HAS differs, and telling
104
+ // it to re-fetch something already in front of it burns a turn for nothing:
105
+ // a blocking image generation returns the pixels inline, a video generation
106
+ // returns none, and a background submission has no asset yet.
107
+ const IMAGE_INLINE_REVIEW = 'The image is attached to this result — look at it against the brief before you describe it. ' +
108
+ 'Any claim about how it LOOKS must come from pixels you actually received (slates-vision-feedback-loop).';
109
+ const VIDEO_REVIEW_POINTER = 'You have NOT seen this clip: call slates_get_asset_video_frames on the asset id above before ' +
110
+ 'describing how it looks. A quality claim you cannot point to a tool result for is a REAL NUMBERS ONLY violation.';
111
+ const BACKGROUND_REVIEW_POINTER = 'When it completes, look at it before you describe it — slates_get_asset_image for images, ' +
112
+ 'slates_get_asset_video_frames for video.';
113
+ // The image saved, but reading it back off disk failed (best-effort fetch). The
114
+ // agent has an asset and NO pixels, which is the one state where a quality
115
+ // claim would be pure invention — so this branch has to say so rather than
116
+ // fall through to no pointer at all.
117
+ const IMAGE_FETCH_POINTER = 'The pixels could not be attached to this result: call slates_get_asset_image on the asset id ' +
118
+ 'above before describing how it looks.';
83
119
  // Early-return shape when a generation route accepted the job in background
84
120
  // mode ({ background: true } in the response). No inline-image fetch — the
85
121
  // asset doesn't exist yet; the poller delivers it on completion.
@@ -88,7 +124,8 @@ function backgroundSubmitted(kind, ids, extra, note) {
88
124
  return {
89
125
  text: `Submitted ${kind} in the background — generationId(s): ${idText}. ` +
90
126
  `Call slates_get_generation_status with waitSeconds: 45 (it long-polls and returns on completion — ` +
91
- `never a rapid loop; video renders commonly take 1-5 minutes). Generations survive app restarts.` +
127
+ `never a rapid loop; video renders commonly take 1-5 minutes). Generations survive app restarts. ` +
128
+ BACKGROUND_REVIEW_POINTER +
92
129
  (note ? ` ${note}` : ''),
93
130
  data: { generationIds: ids, status: 'processing', ...extra },
94
131
  };
@@ -191,6 +228,21 @@ export const VIDEO_MODELS = [
191
228
  // the Max row at base rates and offers it 2K/4K it cannot render.
192
229
  'minimax-h3',
193
230
  'minimax-h3-max',
231
+ // LTX-2.5, two seats in one family (2026-08-29). Base is the VOLUME seat —
232
+ // cheapest native 1080p second we sell, free native audio at every tier, the
233
+ // only row reaching 1440p, and the longest clips in the catalogue (20s).
234
+ // Pro is the fidelity seat and is NOT a superset: shorter ladder (no
235
+ // 1440p/4K), shorter clips (6/8/10 only) and ~1/3 dearer.
236
+ //
237
+ // NEVER PREFIX-MATCH: 'ltx-2-5-pro' starts with 'ltx-2-5'. A prefix test
238
+ // bills Pro at base rates AND offers it 1440p/4K and 12-20s durations it
239
+ // cannot render — the same trap as the MiniMax pair, one row worse.
240
+ //
241
+ // Durations are DISCRETE AND EVEN (6,8,10,12,14,16,18,20); the union bounds
242
+ // below stay 3-30 because other rows are wider, so `assertVideoCapabilities`
243
+ // is what refuses an odd second. It reads `values`, not just min/max.
244
+ 'ltx-2-5',
245
+ 'ltx-2-5-pro',
194
246
  ];
195
247
  // ── Capability-derived param vocabulary + guard ─────────────────
196
248
  //
@@ -231,6 +283,17 @@ const VIDEO_RESOLUTION_VOCAB = VIDEO_RESOLUTIONS;
231
283
  const MINIMAX_MODELS = new Set(['minimax-h3', 'minimax-h3-max']);
232
284
  /** Reference images fal does not charge for. */
233
285
  const MINIMAX_FREE_REF_IMAGES = 5;
286
+ /**
287
+ * The LTX-2.5 pair. A SET, not a prefix test — `ltx-2-5-pro` starts with
288
+ * `ltx-2-5`, and the two rows differ on ladder, duration list AND price.
289
+ *
290
+ * Their cost key is the plainest shape in the file — `{model}-{res}-{N}s` with
291
+ * no suffix ever, because LTX has no paid option: native audio is included at
292
+ * every tier (so no `-audio` variant like Kling) and there is no reference
293
+ * endpoint at all (so no `-ref{K}` variant like H3). Mirrors `ltxCreditKey()`
294
+ * in slate/src/shared/pricing.ts.
295
+ */
296
+ const LTX_MODELS = new Set(['ltx-2-5', 'ltx-2-5-pro']);
234
297
  /** K for the `-ref{K}` suffix: images past the free five, capped by the model's
235
298
  * own declared ceiling. 0 for h3-max (no reference transport) and for anything
236
299
  * that is not a MiniMax row. Mirrors refImageSurchargeCount() in
@@ -1077,7 +1140,17 @@ function imageCostKey(model, resolution, quality = 'medium') {
1077
1140
  }
1078
1141
  export const generateImage = {
1079
1142
  id: 'slates_generate_image',
1080
- description: 'Generate an image via Slates credits. Models: nano-banana-2 (default), nano-banana-2-lite (fast/cheap drafts, 1K only), nano-banana-pro (hero-frame/typography premium), gpt-image-2 (sharp text / character sheets / grids; quality medium|high), flux-2-max (photoreal, less censored), seedream-5-lite (cheapest flat, less censored). Which model for which job: read the slates-model-selection skill. Pass projectId to save into a Slates project (recommended — asset appears live in the desktop UI). All models except nano-banana-2 REQUIRE projectId (no headless path). REQUIRED before calling: read the slates-cost-discipline skill (and the model\'s slates-prompting-* skill). You MUST pass aspectRatio and resolution explicitly (the server returns requires_clarification when missing — defaults waste credits). Cost > $0.50 returns requires_confirm — pass confirm=true after explicit user OK. MCP/CLI generation always charges credits. No skill files installed? Call slates_get_prompting_guide with the model\'s topic (and \'slates-cost-discipline\') before first use.',
1143
+ description: 'Generate an image via Slates credits.\n' +
1144
+ // GENERATED from MODEL_FACTS — the hand-typed model list that stood here
1145
+ // was a third copy of the routing doctrine, and it had already gone stale
1146
+ // (it still described nano-banana-2-lite by a capability the param owns).
1147
+ `${describeRouting('image')}\n` +
1148
+ 'Full table: the slates-model-selection skill. ' +
1149
+ 'Pass projectId to save into a Slates project (recommended — asset appears live in the desktop UI). All models except nano-banana-2 REQUIRE projectId (no headless path). REQUIRED before calling: read the slates-cost-discipline skill (and the model\'s slates-prompting-* skill). You MUST pass aspectRatio and resolution explicitly (the server returns requires_clarification when missing — defaults waste credits). Cost > $0.50 returns requires_confirm — pass confirm=true after explicit user OK. MCP/CLI generation always charges credits. No skill files installed? Call slates_get_prompting_guide with the model\'s topic (and \'slates-cost-discipline\') before first use. ' +
1150
+ // GENERATED from the skill file's own never-use list -- the one piece of
1151
+ // prompting doctrine that is ALWAYS in context, because the agent has
1152
+ // demonstrably skipped the call that would have taught it.
1153
+ describeBannedTokens('image'),
1081
1154
  input: z.object({
1082
1155
  prompt: z.string().min(1).max(4000),
1083
1156
  model: zEnum(IMAGE_MODELS).optional().describe('Image model. Default nano-banana-2. Routing doctrine: slates-model-selection skill. All except nano-banana-2 require projectId.'),
@@ -1096,6 +1169,10 @@ export const generateImage = {
1096
1169
  // Mirrors the cost confirm gate — defaults silently wasted credits
1097
1170
  // (1:1 when user wanted 16:9, 4k when 1k would have done). The LLM
1098
1171
  // is forced to ask the user or read the skill instead of guessing.
1172
+ // Non-blocking prompt hygiene. Computed once, reported on every exit path
1173
+ // that echoes a prompt -- the clarification and confirm gates are PRE-spend,
1174
+ // which is where a rewrite is still free.
1175
+ const promptWarning = bannedTokenWarning(input.prompt, 'image');
1099
1176
  if (!input.aspectRatio || !input.resolution) {
1100
1177
  const missing = [];
1101
1178
  if (!input.aspectRatio)
@@ -1105,7 +1182,9 @@ export const generateImage = {
1105
1182
  return ok({
1106
1183
  requires_clarification: true,
1107
1184
  missing,
1108
- message: `Missing required field(s): ${missing.join(', ')}. ` +
1185
+ ...(promptWarning ? { prompt_warning: promptWarning } : {}),
1186
+ message: (promptWarning ? `${promptWarning}\n\n` : '') +
1187
+ `Missing required field(s): ${missing.join(', ')}. ` +
1109
1188
  `Read the slates-prompting-nano-banana-2 + slates-cost-discipline skills, ` +
1110
1189
  // Generated from MODEL_CAPABILITIES — never retype a ratio list.
1111
1190
  `or ask the user. Aspect ratio by model: ${describeAspectRatios(IMAGE_MODELS)}. ` +
@@ -1189,7 +1268,9 @@ export const generateImage = {
1189
1268
  model: costKey,
1190
1269
  estimated_cents: totalCents,
1191
1270
  estimated_credits: totalCents,
1192
- message: `Cost exceeds ${CONFIRM_CREDITS} credits. Re-call with confirm=true to proceed, or pick a smaller resolution / count.`,
1271
+ ...(promptWarning ? { prompt_warning: promptWarning } : {}),
1272
+ message: (promptWarning ? `${promptWarning}\n\n` : '') +
1273
+ `Cost exceeds ${CONFIRM_CREDITS} credits. Re-call with confirm=true to proceed, or pick a smaller resolution / count.`,
1193
1274
  });
1194
1275
  }
1195
1276
  const previews = await previewAssets(ctx, referenceAssetIds.map((id) => ({ id, type: 'image', role: 'reference' })));
@@ -1201,7 +1282,8 @@ export const generateImage = {
1201
1282
  `Review them against your prompt — every reference's role must be labeled in the prompt text. ` +
1202
1283
  `If the references suggest a different composition / style than the current prompt captures, REVISE the prompt before confirming. ` +
1203
1284
  `When you talk to the user about this gen, refer to each reference by its code (e.g. "${previews[0]?.ref ?? 'IMG-A?'}") — they'll see the matching badge in the Slates gallery.` +
1204
- `\n\nWhen ready, re-call slates_generate_image with confirm=true and the (possibly revised) prompt.`,
1285
+ `\n\nWhen ready, re-call slates_generate_image with confirm=true and the (possibly revised) prompt.` +
1286
+ (promptWarning ? `\n\n${promptWarning}` : ''),
1205
1287
  images: previews.flatMap((p) => p.images),
1206
1288
  data: {
1207
1289
  requires_confirm: true,
@@ -1276,15 +1358,21 @@ export const generateImage = {
1276
1358
  }
1277
1359
  const requestedCount = input.count ?? 1;
1278
1360
  return {
1279
- text: partialFailure
1280
- ? `Partial result: ${assetList.length} of ${requestedCount} image(s) saved into project ${input.projectId} ` +
1281
- `(error on the rest: ${result.error ?? 'unknown error'}). ` +
1282
- `The saved assets are in data.assets — re-generate only the missing count, don't redo the whole batch. ` +
1283
- `Prompt: "${input.prompt.slice(0, 60)}${input.prompt.length > 60 ? '...' : ''}"`
1284
- : `Generated ${assetList.length} image(s) into project ${input.projectId} ` +
1285
- `for ${fmtCredits(totalCents)}. ` +
1286
- `Prompt: "${input.prompt.slice(0, 60)}${input.prompt.length > 60 ? '...' : ''}"` +
1287
- (refEcho ? ` ${refEcho}` : ''),
1361
+ // The pixels are ALREADY here when the disk read worked, so the
1362
+ // review pointer says "look at what you have", not "call another op".
1363
+ // Telling the agent to re-fetch an image already in its context would
1364
+ // buy a wasted turn and teach the wrong habit.
1365
+ text: `${images.length > 0 ? IMAGE_INLINE_REVIEW : IMAGE_FETCH_POINTER} ` +
1366
+ (promptWarning ? `${promptWarning} ` : '') +
1367
+ (partialFailure
1368
+ ? `Partial result: ${assetList.length} of ${requestedCount} image(s) saved into project ${input.projectId} ` +
1369
+ `(error on the rest: ${result.error ?? 'unknown error'}). ` +
1370
+ `The saved assets are in data.assets — re-generate only the missing count, don't redo the whole batch. ` +
1371
+ `Prompt: "${input.prompt.slice(0, 60)}${input.prompt.length > 60 ? '...' : ''}"`
1372
+ : `Generated ${assetList.length} image(s) into project ${input.projectId} ` +
1373
+ `for ${fmtCredits(totalCents)}. ` +
1374
+ `Prompt: "${input.prompt.slice(0, 60)}${input.prompt.length > 60 ? '...' : ''}"` +
1375
+ (refEcho ? ` ${refEcho}` : '')),
1288
1376
  images,
1289
1377
  data: {
1290
1378
  model: imageModel,
@@ -1352,7 +1440,9 @@ export const generateImage = {
1352
1440
  images.push({ data: buf.toString('base64'), mimeType: mt });
1353
1441
  }
1354
1442
  return {
1355
- text: `Generated ${urls.length} image(s) for ${fmtCredits(totalCents)}. ` +
1443
+ text: `${IMAGE_INLINE_REVIEW} ` +
1444
+ (promptWarning ? `${promptWarning} ` : '') +
1445
+ `Generated ${urls.length} image(s) for ${fmtCredits(totalCents)}. ` +
1356
1446
  `Prompt: "${input.prompt.slice(0, 60)}${input.prompt.length > 60 ? '...' : ''}"`,
1357
1447
  images,
1358
1448
  data: {
@@ -1569,6 +1659,16 @@ export function videoCostKey(input) {
1569
1659
  const k = minimaxRefSurchargeCount(input.model, input.referenceImages);
1570
1660
  return `${input.model}-${res}-${input.duration}s${k > 0 ? `-ref${k}` : ''}`;
1571
1661
  }
1662
+ // LTX-2.5, both seats. EXACT-ID SET, NEVER A PREFIX — `ltx-2-5-pro` starts
1663
+ // with `ltx-2-5`, and a prefix match would quote base rates for the dearer
1664
+ // row. No suffix dimension exists: audio is free and there are no references.
1665
+ // The resolution default is read PER ROW (both are 1080p today, but the two
1666
+ // ladders differ, so a shared literal would be a latent bug the day one
1667
+ // moves). Mirrors ltxCreditKey() in slate/src/shared/pricing.ts.
1668
+ if (LTX_MODELS.has(input.model)) {
1669
+ const res = input.videoResolution ?? defaultVideoResolutionFor(input.model);
1670
+ return `${input.model}-${res}-${input.duration}s`;
1671
+ }
1572
1672
  if (input.model.startsWith('seedance')) {
1573
1673
  // Mirrors seedanceCreditKey() in slate/src/shared/pricing.ts (version × face
1574
1674
  // × vref × res × duration). AI-face route bills the `-face-` key (~45% over
@@ -1867,7 +1967,10 @@ function promptingSkillFor(model) {
1867
1967
  }
1868
1968
  export const generateVideo = {
1869
1969
  id: 'slates_generate_video',
1870
- description: 'Generate video via Slates credits. REQUIRED before calling: read slates-model-selection (the routing doctrine), slates-cost-discipline, and the matching per-model prompting skill (slates-prompting-seedance / slates-prompting-seedance-2-5 / slates-prompting-kling-v3 / slates-prompting-veo-3 / slates-prompting-minimax-h3) — video models prompt very differently; load them via slates_get_prompting_guide if no skill files are installed. Read slates-content-policy when the scene involves conflict, creatures, crowds, destruction, weapons, or young characters. projectId, aspectRatio, and duration are required (requires_clarification otherwise). Cost > $0.50 returns requires_confirm — pass confirm=true after explicit user OK. Image-to-video via firstFrameAssetId; first+last frames = Veo/Seedance only; ingredients via ingredientAssetIds (Kling Omni / Seedance). Asset params take UUIDs or badge codes ("IMG-A8").',
1970
+ description: 'Generate video via Slates credits. REQUIRED before calling: read slates-model-selection (the routing doctrine), slates-cost-discipline, and the matching per-model prompting skill (slates-prompting-seedance / slates-prompting-seedance-2-5 / slates-prompting-kling-v3 / slates-prompting-veo-3 / slates-prompting-minimax-h3) — video models prompt very differently; load them via slates_get_prompting_guide if no skill files are installed. Read slates-content-policy when the scene involves conflict, creatures, crowds, destruction, weapons, or young characters. projectId, aspectRatio, and duration are required (requires_clarification otherwise). Cost > $0.50 returns requires_confirm — pass confirm=true after explicit user OK. Image-to-video via firstFrameAssetId; first+last frames = Veo/Seedance only; ingredients via ingredientAssetIds (Kling Omni / Seedance). Asset params take UUIDs or badge codes ("IMG-A8"). ' +
1971
+ // GENERATED from the skill's own slop-token list. Always in context on both
1972
+ // surfaces, so it survives an agent that skips slates_get_prompting_guide.
1973
+ describeBannedTokens('video'),
1871
1974
  input: z.object({
1872
1975
  prompt: z.string().min(1).max(4000),
1873
1976
  // ROUTING doctrine only. Every capability number was stripped on 2026-08-16
@@ -1876,7 +1979,14 @@ export const generateVideo = {
1876
1979
  // "seedance-2.5 480p/720p" were both stated here AND there, and the two
1877
1980
  // copies disagreed — and the second of those went stale on 2026-08-24 when
1878
1981
  // 2.5 gained 1080p, which is exactly the failure mode generating it fixes.
1879
- model: z.string().describe(`One of: ${VIDEO_MODELS.join(' | ')}. Pass the BASE id — duration and videoResolution are separate params (registry cost keys like "kling-v3-standard-8s" auto-resolve). Route per the slates-model-selection skill: Kling std = general-purpose DEFAULT, Seedance 2 = premium physics/effects/hero tier, seedance-2.5 = a SECOND SEAT beside it (longer takes, far more references, audio-only refs — but no 4K, and dearer than seedance-2 at every shared resolution, so stay on seedance-2 unless length or reference count is the point), Veo = native-synced-audio niche only, never the default, omni-flash = cheap tier with audio included (t2v, single-start-frame i2v, or reference images; no last frame, no video/audio refs), minimax-h3 = the AUTHORED-AUDIO seat (dialogue, scene sound and score directed as three separate layers in one pass, plus declared reference relationships; reference images past the fifth are a PAID key dimension — pass referenceImages when quoting), minimax-h3-max = the same model post-trained by fal for SPEED, capped at 768p, and DEARER than minimax-h3 at the tier they share — a deliberate pick, never a default and never the cheap H3; it still takes firstFrameAssetId/lastFrameAssetId, but has no reference endpoint, so the reference set is minimax-h3 only. All are VIDEO-only. Each model's legal aspect ratios, durations and resolutions are in those params' own descriptions — read them there, not from memory. For per-call cost, call slates_estimate_generation_cost.`),
1982
+ model: z.string().describe(`One of: ${VIDEO_MODELS.join(' | ')}. Pass the BASE id — duration and videoResolution are separate ` +
1983
+ `params (registry cost keys like "kling-v3-standard-8s" auto-resolve). All are VIDEO-only.\n` +
1984
+ // GENERATED from MODEL_FACTS. The paragraph that stood here restated it by
1985
+ // hand and had already drifted a phrase at a time.
1986
+ `${describeRouting('video', 'generate')}\n` +
1987
+ `Full table: the slates-model-selection skill. Each model's legal aspect ratios, durations and ` +
1988
+ `resolutions are in those params' own descriptions — read them there, not from memory. ` +
1989
+ `For per-call cost, call slates_estimate_generation_cost.`),
1880
1990
  projectId: z.string().uuid().optional().describe('Save into this Slates project. Strongly recommended — the desktop UI shows a progress card live and the asset appears when complete.'),
1881
1991
  // 🚨 THESE THREE DESCRIPTIONS ARE GENERATED FROM `MODEL_CAPABILITIES`.
1882
1992
  // Never hand-write a ratio, resolution or duration into them again — every
@@ -1940,11 +2050,16 @@ export const generateVideo = {
1940
2050
  // no asset to reference later, and a failed gen leaves the user with
1941
2051
  // nothing. The MCP-only headless path that exists for image gen is
1942
2052
  // not reasonable for video given the cost.
2053
+ // Non-blocking prompt hygiene, computed once. Reported on the gates that
2054
+ // fire BEFORE any spend, where a rewrite is still free.
2055
+ const promptWarning = bannedTokenWarning(input.prompt, 'video');
1943
2056
  if (!input.projectId) {
1944
2057
  return ok({
1945
2058
  requires_clarification: true,
1946
2059
  missing: ['projectId'],
1947
- message: 'projectId is required for video generation. Use slates_list_projects to find one or slates_create_project to make a new one. Video gens cost tens to a few hundred credits per call — they need to land in a project so the user sees the progress card and the result.',
2060
+ ...(promptWarning ? { prompt_warning: promptWarning } : {}),
2061
+ message: (promptWarning ? `${promptWarning}\n\n` : '') +
2062
+ 'projectId is required for video generation. Use slates_list_projects to find one or slates_create_project to make a new one. Video gens cost tens to a few hundred credits per call — they need to land in a project so the user sees the progress card and the result.',
1948
2063
  });
1949
2064
  }
1950
2065
  if (!input.aspectRatio || !input.duration) {
@@ -1956,7 +2071,9 @@ export const generateVideo = {
1956
2071
  return ok({
1957
2072
  requires_clarification: true,
1958
2073
  missing,
1959
- message: `Missing required field(s): ${missing.join(', ')}. ` +
2074
+ ...(promptWarning ? { prompt_warning: promptWarning } : {}),
2075
+ message: (promptWarning ? `${promptWarning}\n\n` : '') +
2076
+ `Missing required field(s): ${missing.join(', ')}. ` +
1960
2077
  `Read the slates-cost-discipline + ${promptingSkillFor(input.model)} skills, ` +
1961
2078
  // Generated from MODEL_CAPABILITIES. The prose that stood here claimed
1962
2079
  // "Veo locks to 16:9", "Kling/Seedance support 1:1 16:9 9:16 4:3 3:4
@@ -2002,6 +2119,34 @@ export const generateVideo = {
2002
2119
  });
2003
2120
  }
2004
2121
  }
2122
+ // LTX-2.5's SHAPE constraint: FRAMES, NEVER REFERENCES. fal publishes
2123
+ // text-to-video and image-to-video for `lightricks/ltx-2.5` and nothing
2124
+ // else — there is no reference-to-video endpoint on either seat, so there
2125
+ // is no transport for an ingredient, character, environment, style,
2126
+ // reference-video or reference-audio input.
2127
+ //
2128
+ // This has to be an EXPLICIT refusal rather than a silent no-op: accepting
2129
+ // reference ids we cannot send is the exact "no error, no warning, no
2130
+ // images in the request" failure `seedance-2.5-edit` shipped with. Start
2131
+ // and end FRAMES are unaffected — image-to-video carries `image_url` plus
2132
+ // an optional `end_image_url`, which is what `features.lastFrame` records.
2133
+ if (LTX_MODELS.has(input.model)) {
2134
+ const refImages = (input.ingredientAssetIds?.length ?? 0) +
2135
+ (input.characterAssetIds?.length ?? 0) +
2136
+ (input.environmentAssetIds?.length ?? 0) +
2137
+ (input.styleAssetIds?.length ?? 0);
2138
+ const refMedia = (input.videoReferenceAssetIds?.length ?? 0) +
2139
+ (input.audioReferenceAssetIds?.length ?? 0) +
2140
+ (input.videoReferenceAssetId ? 1 : 0) +
2141
+ (input.audioReferenceAssetId ? 1 : 0);
2142
+ if (refImages > 0 || refMedia > 0) {
2143
+ return ok({
2144
+ requires_clarification: true,
2145
+ missing: [],
2146
+ message: `${input.model} takes a prompt and up to two frames (start and/or end) — it has no reference endpoint at all, so reference images, video and audio cannot be sent. Drop them, or switch to minimax-h3, which reads ${getModelCapability('minimax-h3')?.maxIngredientImages ?? 9} images plus reference video and audio.`,
2147
+ });
2148
+ }
2149
+ }
2005
2150
  // MiniMax H3's SHAPE constraints. Counts and caps are read from the
2006
2151
  // capability SSOT; only the endpoint SHAPE is stated here, because it is
2007
2152
  // not a number the registry models: fal publishes text-to-video,
@@ -2268,6 +2413,7 @@ export const generateVideo = {
2268
2413
  text: `Pre-flight for ${input.duration}s ${input.model} (${costKey}): ` +
2269
2414
  `${fmtCredits(totalCents)}.` +
2270
2415
  refSummary +
2416
+ (promptWarning ? `\n\n${promptWarning}` : '') +
2271
2417
  `\n\nWhen ready, re-call slates_generate_video with confirm=true and the (possibly revised) prompt.`,
2272
2418
  images: refImages,
2273
2419
  data: {
@@ -2346,7 +2492,12 @@ export const generateVideo = {
2346
2492
  }, refEcho);
2347
2493
  }
2348
2494
  return {
2349
- text: `Generated ${input.duration}s ${input.model} video into project ${input.projectId} ` +
2495
+ text:
2496
+ // No frames come back with a video generation, so unlike the image op
2497
+ // this one has to name the op that fetches them.
2498
+ `${VIDEO_REVIEW_POINTER} ` +
2499
+ (promptWarning ? `${promptWarning} ` : '') +
2500
+ `Generated ${input.duration}s ${input.model} video into project ${input.projectId} ` +
2350
2501
  `for ${fmtCredits(totalCents)}. ` +
2351
2502
  `Prompt: "${input.prompt.slice(0, 60)}${input.prompt.length > 60 ? '...' : ''}"` +
2352
2503
  (refEcho ? ` ${refEcho}` : ''),
@@ -2375,7 +2526,10 @@ export const generateAudio = {
2375
2526
  projectId: z.string().uuid().describe('Slates project the audio asset lands in. Required — the renderer refreshes live.'),
2376
2527
  model: z
2377
2528
  .enum(AUDIO_MODELS)
2378
- .describe('Audio surface. seed-audio = scene/ambience/beds/dialogue (default choice), eleven-sfx = one precise effect or a seamless loop. Routing doctrine: slates-model-selection skill.'),
2529
+ .describe(
2530
+ // GENERATED from MODEL_FACTS — the audio lane's routing lived only in the
2531
+ // system prompt and in a one-line paraphrase here.
2532
+ `Audio surface.\n${describeRouting('audio')}\nFull table: the slates-model-selection skill.`),
2379
2533
  prompt: z
2380
2534
  .string()
2381
2535
  .min(1)
@@ -2743,7 +2897,13 @@ export const editVideo = {
2743
2897
  sourceVideoAssetId: z.string().describe('The VIDEO asset to edit — UUID or badge code ("VID-V3", bare "V3"); codes resolve against the project at call time. Kling: 3–15s clips; omni-flash-edit: 3–10s.'),
2744
2898
  prompt: z.string().min(1).max(2500).describe('The change, not the whole scene — e.g. "replace the man with @marcus", "make it a rainy night", "turn the street into a neon Tokyo alley". Mention subjects with @name; the transport compiles them to Kling\'s @ElementN notation (Kling models). For omni-flash-edit keep it simple and add "Keep everything else the same."'),
2745
2899
  // Clip windows and resolutions GENERATED from MODEL_CAPABILITIES.
2746
- model: zEnum(EDIT_VIDEO_MODELS).optional().describe(`Default kling-v3.0-omni-edit. Pro (~25¢/s vs ~19¢/s) only for hero shots where fidelity matters. omni-flash-edit (~19¢/s) for prompt-only edits — it takes NO character/style refs. seedance-2.5-edit is the ONLY engine that accepts a clip longer than 15s; it also takes NO character/style refs on this op. Source-clip length per engine: ${describeDurations(EDIT_VIDEO_MODELS)}. Output resolution: ${describeVideoResolutions(EDIT_VIDEO_MODELS)}`),
2900
+ model: zEnum(EDIT_VIDEO_MODELS).optional().describe(
2901
+ // Routing GENERATED from MODEL_FACTS; the paragraph that stood here was
2902
+ // hand-written and carried per-second prices, which drift and which the
2903
+ // agent's own REAL NUMBERS ONLY rule forbids it repeating.
2904
+ `Default kling-v3.0-omni-edit. Neither omni-flash-edit nor seedance-2.5-edit takes character/style refs on this op.\n` +
2905
+ `${describeRouting('video', 'edit')}\n` +
2906
+ `Source-clip length per engine: ${describeDurations(EDIT_VIDEO_MODELS)}. Output resolution: ${describeVideoResolutions(EDIT_VIDEO_MODELS)}`),
2747
2907
  characterAssetIds: z.array(z.string()).max(4).optional().describe('Subject/element image assets to swap IN (UUIDs or badge codes). Each becomes a Kling element (@ElementN). KLING MODELS ONLY — rejected on omni-flash-edit.'),
2748
2908
  styleAssetIds: z.array(z.string()).max(4).optional().describe('Style/appearance reference images (@ImageN). Max 4 combined with characterAssetIds. KLING MODELS ONLY — rejected on omni-flash-edit.'),
2749
2909
  keepAudio: z.boolean().optional().describe('Preserve the original audio track (default true; Kling models only — omni-flash-edit and seedance-2.5-edit output carry their own audio).'),
@@ -3578,6 +3738,12 @@ function resolveGuideTopic(topic) {
3578
3738
  t.startsWith('h3-')) {
3579
3739
  return 'slates-prompting-minimax-h3';
3580
3740
  }
3741
+ // LTX-2.5 — both seats share one skill. A PREFIX is right HERE (unlike every
3742
+ // rate, key and endpoint lookup, which must be exact) precisely because both
3743
+ // rows resolve to the same guide: `ltx-2-5-pro` matching the `ltx` prefix is
3744
+ // the intended outcome, not a collision.
3745
+ if (t.startsWith('ltx') || t === 'lightricks')
3746
+ return 'slates-prompting-ltx-2-5';
3581
3747
  if (t.startsWith('kling-mc'))
3582
3748
  return 'slates-prompting-motion-transfer';
3583
3749
  if (t === 'edit-video' || t === 'video-edit' || t === 'edit video' || t === 'video edit')
@@ -3630,6 +3796,27 @@ function resolveGuideTopic(topic) {
3630
3796
  }
3631
3797
  return null;
3632
3798
  }
3799
+ /**
3800
+ * The guide index, GENERATED from SKILLS.
3801
+ *
3802
+ * The list here was hand-typed and had drifted to 25 of 32 names — the prompting
3803
+ * guides for GPT Image 2, MiniMax H3, LTX-2.5, Seedance 2.5 and Omni Flash were
3804
+ * all missing, so an agent reading this description could not learn they exist.
3805
+ * A hand-typed index of a generated corpus is a stale index; it is only a matter
3806
+ * of when.
3807
+ *
3808
+ * Per-model guides are listed as BARE NAMES: the name is the description, and
3809
+ * `resolveGuideTopic()` resolves a model id to the right one anyway.
3810
+ */
3811
+ function describeGuideTopics() {
3812
+ const names = Object.keys(SKILLS).sort();
3813
+ const perModel = names.filter((n) => n.startsWith('slates-prompting-'));
3814
+ const rest = names.filter((n) => !n.startsWith('slates-prompting-'));
3815
+ return (`Workflow and cross-cutting guides: ${rest.join(', ')}. ` +
3816
+ `Per-model prompting guides (or just pass the model id, which resolves to one of these): ` +
3817
+ `${perModel.join(', ')}. ` +
3818
+ `Style names (photoreal, anime, painterly, 3d-render) resolve to slates-style-prompting.`);
3819
+ }
3633
3820
  export const getPromptingGuide = {
3634
3821
  id: 'slates_get_prompting_guide',
3635
3822
  description: "Return the full markdown of a bundled Slates prompting/workflow guide. MCP-only clients (Claude Desktop, Smithery) don't get the CLI-installed skill files — call this instead. Accepts a guide name or a model id (e.g. 'veo-3.1-fast', 'kling-v3.0-pro', 'seedance-2', 'nano-banana-2') which maps to the right guide. ALWAYS read 'slates-cost-discipline' plus the relevant model guide before your first generation in a session.",
@@ -3637,7 +3824,7 @@ export const getPromptingGuide = {
3637
3824
  topic: z
3638
3825
  .string()
3639
3826
  .min(1)
3640
- .describe('Guide name, model id, or style name. Guides: slates-model-selection (which model for which job — read before choosing any model), slates-cost-discipline, slates-content-policy, slates-style-prompting, slates-prompting-nano-banana-2, slates-prompting-veo-3, slates-prompting-kling-v3, slates-prompting-seedance, slates-prompting-seed-audio, slates-prompting-elevenlabs, slates-prompting-lip-sync, slates-prompting-motion-transfer, slates-prompting-flux-2-max, slates-prompting-seedream-5-lite, slates-edit-and-iterate, slates-vision-feedback-loop, slates-character-identity, slates-storyboard-from-script, slates-direct-response-ad, slates-one-prompt-film. Blender previs (3D-blocked camera control): slates-previs-blocking (start here), slates-camera-language, slates-blocking-to-prompt, slates-dialogue-blocking, slates-restyle-from-blocking. Style names (photoreal, anime, painterly, 3d-render) resolve to slates-style-prompting.'),
3827
+ .describe(`Guide name, model id, or style name. ${describeGuideTopics()}`),
3641
3828
  }),
3642
3829
  async run(input) {
3643
3830
  const resolved = resolveGuideTopic(input.topic);
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Which agent surface is being briefed.
3
+ *
4
+ * `desktop` — the in-app Studio Agent. Its loop enforces plan approval in
5
+ * code, auto-polls generation status, and displays orchestration cost itself.
6
+ * `mcp` — Claude Code / Claude Desktop / Cursor / Codex over stdio. No
7
+ * `present_plan` tool, no auto-poll, no app chrome; the consent gate is the
8
+ * op-level `requires_confirm` threshold plus the host client's own per-call
9
+ * tool approval. The asymmetry is DELIBERATE — see slates-mcp/CLAUDE.md.
10
+ */
11
+ export type AgentSurface = 'desktop' | 'mcp';
12
+ interface SkillIndexEntry {
13
+ name: string;
14
+ description: string;
15
+ }
16
+ /** Parse `name:`/`description:` out of each embedded skill's frontmatter. */
17
+ export declare function buildSkillIndex(): SkillIndexEntry[];
18
+ export declare const WORKING_METHOD: ReadonlyArray<Record<AgentSurface, string>>;
19
+ export declare const HARD_RULES: ReadonlyArray<Record<AgentSurface, string>>;
20
+ /**
21
+ * THE doctrine string for a surface. The desktop's whole system prompt and the
22
+ * MCP server's `instructions` are both exactly this — neither consumer adds
23
+ * doctrine prose of its own.
24
+ *
25
+ * SENTINEL: the literal below must appear in this workspace in exactly ONE
26
+ * file — this one. scripts/agent-surface-lockstep-check.mjs assembles the same
27
+ * string from its parts rather than writing it out, precisely so that grepping
28
+ * for it stays a meaningful question. It is how "no consumer grew a private
29
+ * copy of the doctrine" is proved rather than assumed.
30
+ * SLATES-AGENT-DOCTRINE-SSOT
31
+ */
32
+ export declare function buildAgentDoctrine({ surface }: {
33
+ surface: AgentSurface;
34
+ }): string;
35
+ export {};
36
+ //# sourceMappingURL=agent-doctrine.d.ts.map