plotlink-ows 1.0.33 → 1.2.94

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.
Files changed (145) hide show
  1. package/README.md +4 -0
  2. package/app/lib/agent-command.ts +85 -0
  3. package/app/lib/agent-readiness.ts +133 -0
  4. package/app/lib/apply-schema.ts +55 -0
  5. package/app/lib/bubble-text.ts +160 -0
  6. package/app/lib/cartoon-coach.ts +198 -0
  7. package/app/lib/cartoon-markdown.ts +83 -0
  8. package/app/lib/cartoon-prompt.ts +122 -0
  9. package/app/lib/cartoon-readiness.ts +811 -0
  10. package/app/lib/clean-image-sync.ts +245 -0
  11. package/app/lib/codex-images.ts +152 -0
  12. package/app/lib/cut-asset-diagnostics.ts +120 -0
  13. package/app/lib/cuts.ts +302 -0
  14. package/app/lib/fonts.ts +109 -0
  15. package/app/lib/generate-claude-md.ts +8 -1
  16. package/app/lib/generate-story-instructions.ts +731 -0
  17. package/app/lib/image-asset-validate.ts +123 -0
  18. package/app/lib/lettering-status.ts +133 -0
  19. package/app/lib/overlays.ts +637 -0
  20. package/app/lib/paths.ts +10 -0
  21. package/app/lib/public-title.ts +65 -0
  22. package/app/lib/publish.ts +16 -2
  23. package/app/lib/story-progress.ts +243 -0
  24. package/app/lib/terminal-protocol.ts +16 -0
  25. package/app/lib/terminal-redact.ts +50 -0
  26. package/app/prisma/schema.sql +25 -0
  27. package/app/routes/agent.ts +42 -0
  28. package/app/routes/codex-images.ts +67 -0
  29. package/app/routes/publish.ts +203 -22
  30. package/app/routes/stories.ts +961 -5
  31. package/app/routes/terminal.ts +383 -31
  32. package/app/server.ts +47 -12
  33. package/app/vite.config.ts +6 -0
  34. package/app/web/components/CartoonPreview.tsx +267 -0
  35. package/app/web/components/CartoonPublishPage.tsx +407 -0
  36. package/app/web/components/CartoonPublishPreview.tsx +121 -0
  37. package/app/web/components/CartoonStepGuide.tsx +90 -0
  38. package/app/web/components/CartoonWorkflowNav.tsx +68 -0
  39. package/app/web/components/CodexImportPicker.tsx +230 -0
  40. package/app/web/components/CutListPanel.tsx +1299 -0
  41. package/app/web/components/EpisodesPage.tsx +80 -0
  42. package/app/web/components/FinishEpisodePanel.tsx +151 -0
  43. package/app/web/components/Layout.tsx +7 -4
  44. package/app/web/components/LetteringEditor.tsx +1141 -0
  45. package/app/web/components/PreviewPanel.tsx +951 -78
  46. package/app/web/components/Settings.tsx +63 -0
  47. package/app/web/components/StoriesPage.tsx +710 -33
  48. package/app/web/components/StoryBrowser.tsx +22 -14
  49. package/app/web/components/StoryInfoPage.tsx +266 -0
  50. package/app/web/components/StoryProgressPanel.tsx +516 -0
  51. package/app/web/components/TerminalPanel.tsx +233 -11
  52. package/app/web/components/WorkflowCoach.tsx +128 -0
  53. package/app/web/components/asset-image.tsx +114 -0
  54. package/app/web/components/asset-test-utils.ts +44 -0
  55. package/app/web/components/export-cut.ts +320 -0
  56. package/app/web/dist/assets/export-cut-nKQ_n2-J.js +1 -0
  57. package/app/web/dist/assets/index-BAZGwVwj.js +143 -0
  58. package/app/web/dist/assets/index-DoXH2OlP.css +32 -0
  59. package/app/web/dist/index.html +2 -2
  60. package/app/web/lib/cartoon-publish-summary.ts +43 -0
  61. package/app/web/lib/codex-import.ts +94 -0
  62. package/app/web/lib/image-compress.ts +53 -0
  63. package/app/web/lib/import-image.ts +58 -0
  64. package/app/web/lib/publish-helpers.ts +385 -0
  65. package/app/web/lib/upload-retry.ts +130 -0
  66. package/app/web/lib/verify-public-title.ts +105 -0
  67. package/app/web/styles.css +9 -0
  68. package/bin/plotlink-ows.js +53 -16
  69. package/bin/startup-plan.cjs +58 -0
  70. package/lib/genres.ts +92 -0
  71. package/package.json +60 -20
  72. package/scripts/gen-schema-sql.mjs +49 -0
  73. package/scripts/package-hygiene.mjs +116 -0
  74. package/scripts/preflight.mjs +173 -0
  75. package/scripts/start-smoke.mjs +128 -0
  76. package/app/node_modules/.prisma/local-client/client.d.ts +0 -1
  77. package/app/node_modules/.prisma/local-client/client.js +0 -5
  78. package/app/node_modules/.prisma/local-client/default.d.ts +0 -1
  79. package/app/node_modules/.prisma/local-client/default.js +0 -5
  80. package/app/node_modules/.prisma/local-client/edge.d.ts +0 -1
  81. package/app/node_modules/.prisma/local-client/edge.js +0 -184
  82. package/app/node_modules/.prisma/local-client/index-browser.js +0 -173
  83. package/app/node_modules/.prisma/local-client/index.d.ts +0 -3304
  84. package/app/node_modules/.prisma/local-client/index.js +0 -207
  85. package/app/node_modules/.prisma/local-client/libquery_engine-darwin-arm64.dylib.node +0 -0
  86. package/app/node_modules/.prisma/local-client/package.json +0 -183
  87. package/app/node_modules/.prisma/local-client/query_engine_bg.js +0 -2
  88. package/app/node_modules/.prisma/local-client/query_engine_bg.wasm +0 -0
  89. package/app/node_modules/.prisma/local-client/runtime/edge-esm.js +0 -35
  90. package/app/node_modules/.prisma/local-client/runtime/edge.js +0 -35
  91. package/app/node_modules/.prisma/local-client/runtime/index-browser.d.ts +0 -370
  92. package/app/node_modules/.prisma/local-client/runtime/index-browser.js +0 -17
  93. package/app/node_modules/.prisma/local-client/runtime/library.d.ts +0 -3982
  94. package/app/node_modules/.prisma/local-client/runtime/library.js +0 -147
  95. package/app/node_modules/.prisma/local-client/runtime/react-native.js +0 -84
  96. package/app/node_modules/.prisma/local-client/runtime/wasm-compiler-edge.js +0 -85
  97. package/app/node_modules/.prisma/local-client/runtime/wasm-engine-edge.js +0 -38
  98. package/app/node_modules/.prisma/local-client/schema.prisma +0 -21
  99. package/app/node_modules/.prisma/local-client/wasm-edge-light-loader.mjs +0 -5
  100. package/app/node_modules/.prisma/local-client/wasm-worker-loader.mjs +0 -5
  101. package/app/node_modules/.prisma/local-client/wasm.d.ts +0 -1
  102. package/app/node_modules/.prisma/local-client/wasm.js +0 -191
  103. package/app/web/dist/assets/index-B-2Ft7Yv.css +0 -32
  104. package/app/web/dist/assets/index-DxATSk7X.js +0 -134
  105. package/packages/cli/node_modules/commander/LICENSE +0 -22
  106. package/packages/cli/node_modules/commander/Readme.md +0 -1149
  107. package/packages/cli/node_modules/commander/esm.mjs +0 -16
  108. package/packages/cli/node_modules/commander/index.js +0 -24
  109. package/packages/cli/node_modules/commander/lib/argument.js +0 -149
  110. package/packages/cli/node_modules/commander/lib/command.js +0 -2662
  111. package/packages/cli/node_modules/commander/lib/error.js +0 -39
  112. package/packages/cli/node_modules/commander/lib/help.js +0 -709
  113. package/packages/cli/node_modules/commander/lib/option.js +0 -367
  114. package/packages/cli/node_modules/commander/lib/suggestSimilar.js +0 -101
  115. package/packages/cli/node_modules/commander/package-support.json +0 -16
  116. package/packages/cli/node_modules/commander/package.json +0 -82
  117. package/packages/cli/node_modules/commander/typings/esm.d.mts +0 -3
  118. package/packages/cli/node_modules/commander/typings/index.d.ts +0 -1045
  119. package/packages/cli/node_modules/resolve-from/index.d.ts +0 -31
  120. package/packages/cli/node_modules/resolve-from/index.js +0 -47
  121. package/packages/cli/node_modules/resolve-from/license +0 -9
  122. package/packages/cli/node_modules/resolve-from/package.json +0 -36
  123. package/packages/cli/node_modules/resolve-from/readme.md +0 -72
  124. package/packages/cli/node_modules/tsup/LICENSE +0 -21
  125. package/packages/cli/node_modules/tsup/README.md +0 -75
  126. package/packages/cli/node_modules/tsup/assets/cjs_shims.js +0 -13
  127. package/packages/cli/node_modules/tsup/assets/esm_shims.js +0 -9
  128. package/packages/cli/node_modules/tsup/assets/package.json +0 -3
  129. package/packages/cli/node_modules/tsup/dist/chunk-DI5BO6XE.js +0 -153
  130. package/packages/cli/node_modules/tsup/dist/chunk-JZ25TPTY.js +0 -42
  131. package/packages/cli/node_modules/tsup/dist/chunk-PEEXUWMS.js +0 -6
  132. package/packages/cli/node_modules/tsup/dist/chunk-TWFEYLU4.js +0 -352
  133. package/packages/cli/node_modules/tsup/dist/chunk-VGC3FXLU.js +0 -203
  134. package/packages/cli/node_modules/tsup/dist/cli-default.js +0 -12
  135. package/packages/cli/node_modules/tsup/dist/cli-main.js +0 -8
  136. package/packages/cli/node_modules/tsup/dist/cli-node.js +0 -14
  137. package/packages/cli/node_modules/tsup/dist/index.d.ts +0 -511
  138. package/packages/cli/node_modules/tsup/dist/index.js +0 -1711
  139. package/packages/cli/node_modules/tsup/dist/rollup.js +0 -6949
  140. package/packages/cli/node_modules/tsup/package.json +0 -99
  141. package/packages/cli/node_modules/tsup/schema.json +0 -362
  142. package/public/screenshot-1.png +0 -0
  143. package/public/screenshot-2.png +0 -0
  144. package/public/screenshot-3.png +0 -0
  145. package/scripts/e2e-verify.ts +0 -1100
@@ -0,0 +1,731 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import type { AgentProvider } from "../routes/stories";
4
+
5
+ function fictionInstructions(): string {
6
+ return `# Writing Instructions — Fiction
7
+
8
+ > Auto-generated by PlotLink OWS. Do not edit manually.
9
+ > For API endpoints and publish details, see ~/.plotlink-ows/CLAUDE.md.
10
+
11
+ ## Story File Structure
12
+
13
+ \`\`\`
14
+ structure.md — Story outline, characters, and arc
15
+ genesis.md — Synopsis hook (~1000 chars)
16
+ plot-01.md — Chapter 1 (max 10K chars)
17
+ plot-02.md — Chapter 2
18
+ ...
19
+ \`\`\`
20
+
21
+ ## structure.md Format
22
+
23
+ ### Required Sections
24
+
25
+ 1. **Core Concept** — One paragraph describing the premise
26
+ 2. **Main Characters** — For each character:
27
+ - Age, Personality, Flaw, Arc
28
+ 3. **Story Arc** — Beginning, Middle, End
29
+ 4. **Chapter Plan** — Numbered list of planned chapters with one-line descriptions
30
+ 5. **Progress Log** — Track what has been written
31
+
32
+ ## genesis.md Format
33
+
34
+ The genesis is the story's hook — the first thing readers see on-chain.
35
+
36
+ - ~1000 characters (hard limit for on-chain genesis)
37
+ - Create immediate intrigue or emotional hook
38
+ - Introduce the core premise without spoilers
39
+ - End with a question or tension that pulls readers forward
40
+
41
+ ## plot-NN.md Format
42
+
43
+ Each chapter is a self-contained prose section:
44
+
45
+ - Maximum 10,000 characters per file
46
+ - Number sequentially: plot-01.md, plot-02.md, etc.
47
+ - Each chapter should advance the story meaningfully
48
+ - Illustrations: upload via API, embed as \`![description](url)\` in markdown
49
+
50
+ ## Publishing Rules
51
+
52
+ - Content is **immutable after publish** — verify everything in Preview first
53
+ - Genesis publishes via \`createStoryline\` (one-time per story)
54
+ - Plots publish via \`chainPlot\` (one per chapter)
55
+ - Always check character count before publishing (500–10,000 chars)
56
+
57
+ ## Security — never print secrets into the terminal
58
+
59
+ - Do NOT print, \`echo\`, \`cat\`, or log auth material in this terminal: no
60
+ \`Authorization: Bearer\` headers or session tokens, no \`OWS_PASSPHRASE\`, and no
61
+ login passphrase or any login command that contains it.
62
+ - The app stores the passphrase in \`~/.plotlink-ows/.env\` and authenticates for
63
+ you — you never need to read or echo it. Use the documented API; keep secrets
64
+ out of command output.
65
+ `;
66
+ }
67
+
68
+ /**
69
+ * Provider-branched clean-image workflow section for cartoon instructions.
70
+ *
71
+ * The clean-image-first rules (no dialogue/SFX/bubbles/watermark/signature baked
72
+ * into art) are shared; only the "who creates the file" contract differs:
73
+ *
74
+ * - Codex: creating the real `assets/plot-NN/cut-XX-clean.webp` file is the
75
+ * PRIMARY instruction, but it is gated behind a capability/checkpoint guardrail
76
+ * (#307) so the session never sits in an indefinite `Working` state: Codex
77
+ * makes one bounded attempt, and if image generation is unavailable it reports
78
+ * the blocker and falls back to the manual prompt + import path instead of
79
+ * hanging. Critically, Codex is never told it cannot create image files (#274)
80
+ * — only to verify-and-report rather than stall.
81
+ * - Claude / legacy (absent provider): Claude does not generate image files in the
82
+ * terminal, so the manual prompt + import handoff stays primary (unchanged
83
+ * behavior).
84
+ */
85
+ function cleanImageWorkflowSection(provider: AgentProvider): string {
86
+ if (provider === "codex") {
87
+ return `### Write the planning files FIRST — never leave a story shell (#362)
88
+
89
+ Before you generate (or attempt to generate) ANY image, write the episode's text
90
+ artifacts to the story folder so the story is never left as just \`.story.json\` +
91
+ \`CLAUDE.md\` if image generation stalls or is unavailable:
92
+
93
+ 1. \`structure.md\` (outline, Character Bible, Visual Style Guide)
94
+ 2. \`genesis.md\` (the reader-facing prologue/opening)
95
+ 3. \`plot-NN.cuts.json\` (the shot-by-shot cut plan, with its real episode \`title\`)
96
+
97
+ Image generation is the LAST step, not the first. A pilot once spent >10 minutes
98
+ attempting image generation and left the folder with no \`structure.md\`,
99
+ \`genesis.md\`, or \`plot-NN.cuts.json\` at all — that is the failure this rule
100
+ prevents. Save the planning files, confirm they exist in the story folder, and
101
+ only then move on to images. (\`plot-NN.md\` is the exception — OWS generates it
102
+ from cuts.json after final images upload; never hand-write it.)
103
+
104
+ ### Before generating images: confirm capability, checkpoint, and never hang (#307)
105
+
106
+ Image generation in this session is NOT guaranteed to be available. Before you
107
+ rely on it, follow this guardrail so the terminal never sits in an indefinite
108
+ \`Working\` state with no files and no blocker report:
109
+
110
+ 1. **Checkpoint first, then make ONE bounded attempt.** Before generating, post a
111
+ one-line status naming the file you are about to create (e.g. "generating
112
+ cut-01 clean image"). Make exactly ONE attempt per file — never retry image
113
+ generation in a loop or wait indefinitely for it to finish.
114
+ 2. **Confirm the capability on that attempt.** If there is no working in-session
115
+ image-generation capability — the attempt errors, returns no usable image
116
+ bytes, or you cannot confirm it quickly — treat image generation as
117
+ UNAVAILABLE for this session. (Per Asset Tooling, never shell out to
118
+ \`magick\`/\`sharp\`/Playwright to fake it.)
119
+ 3. **Fail visibly, never silently.** The moment image generation is
120
+ unavailable/blocked, report it in one clear line — "Image generation is
121
+ unavailable in this Codex session; switching to the prompt-and-import
122
+ handoff." — then use the Fallback below and stop cleanly. An unreported hang
123
+ is a bug; a clear blocker report is the correct, expected outcome.
124
+ 4. **Report progress per file.** After each saved image or each blocker, post a
125
+ one-line checkpoint ("cut 2/6 saved" / "cover saved" / "blocked: <reason>") so
126
+ the writer always sees progress, never a bare spinner.
127
+
128
+ This guardrail applies to BOTH the cover and every clean cut image. Creating the
129
+ files is still your primary job when generation works (below); the rule is only
130
+ that you verify-and-report instead of stalling.
131
+
132
+ ### Create the clean image file directly — your primary job
133
+
134
+ When image generation is available (confirm it per the guardrail above), for each
135
+ cut you CREATE THE REAL CLEAN-IMAGE FILE — you do not just return a prompt or
136
+ description. How that image reaches the cut depends on the format your image tool
137
+ produces — there are TWO acceptable outcomes, and a generated PNG is one of them:
138
+
139
+ 1. Generate the clean image for the requested cut from its shot type + description
140
+ + characters (the OWS UI's per-cut "Copy Codex task" / "Copy prompt" gives you
141
+ the exact visual prompt). Built-in image generation usually writes a **PNG** into
142
+ a generation cache such as \`~/.codex/generated_images/…\`, NOT your story folder —
143
+ that is an EXPECTED, acceptable result, not a failure. Do NOT try to convert it
144
+ yourself (the Asset Tooling ban forbids \`magick\`/\`sharp\`).
145
+ 2. **Get the image onto the cut — pick the path that matches the file you have:**
146
+ - **Already WebP or JPEG, under 1MB:** finalize it into the OWS asset path. Copy
147
+ or move the file to \`assets/plot-NN/cut-XX-clean.webp\` — use the episode's
148
+ \`plot-NN\` folder and the cut's zero-padded id (\`cut-01\`, \`cut-02\`, …). A plain
149
+ file copy/move (\`cp\`/\`mv\`) is fine — the Asset Tooling ban is only on using
150
+ \`magick\`/\`sharp\`/Playwright to resize/convert/composite/measure. Then run the
151
+ OWS "Sync clean images" action so OWS records \`cleanImagePath\`.
152
+ - **A PNG (the common case), or any file OWS will not take directly:** do NOT
153
+ convert it and do NOT rename a \`.png\` to \`.webp\` — a PNG saved under a \`.webp\`
154
+ name is not a valid clean asset, and OWS rejects it on its content check. Leave
155
+ the file where it is and report it on an \`IMPORT NEEDED\` line (below); the
156
+ writer imports it into that exact cut with the OWS per-cut **"Import from
157
+ Codex"** button, which converts the generated PNG to a compliant WebP
158
+ automatically and records \`cleanImagePath\`. Importing a generated PNG this way
159
+ is a normal, supported finish — not a fallback, and not something you should
160
+ keep retrying to avoid.
161
+ 3. The image must contain NO text, captions, speech bubbles, SFX lettering,
162
+ readable signage, watermark, or signature.
163
+ 4. **Validate at the OWS path before reporting success.** A cut counts as SAVED only
164
+ when \`assets/plot-NN/cut-XX-clean.webp\` really exists — e.g.
165
+ \`find assets/plot-NN -name 'cut-XX-clean.*'\`, with \`file\` reporting WebP or JPEG
166
+ under 1MB (existence/type/size checks like \`find\`/\`file\` are allowed — they don't
167
+ manipulate the image). A cut whose image is still a PNG in
168
+ \`~/.codex/generated_images/…\` is NOT "saved" — list it as \`IMPORT NEEDED\` so the
169
+ writer imports it via "Import from Codex", and never hand-write a \`cleanImagePath\`
170
+ for a file that is not really at the OWS path.
171
+
172
+ **Only exception:** Include text in an image when it is part of the physical scene
173
+ (a sign on a building, text on a screen, a letter being read) AND the writer has
174
+ explicitly requested it.
175
+
176
+ ### Fallback: hand the prompt to the writer (only if image generation is unavailable)
177
+
178
+ If image generation is not available in this session, do not block — fall back to
179
+ the manual handoff for each cut:
180
+ 1. PREPARE THE EXACT CLEAN-IMAGE PROMPT (shot type + description + characters +
181
+ the no-text constraint). This is the same prompt the OWS UI exposes per cut.
182
+ 2. Tell the writer to generate it externally (any provider/tool) and then
183
+ upload/import the resulting WebP/JPEG into OWS using the per-cut "Copy prompt" +
184
+ "Upload clean image" controls.
185
+ 3. **Do NOT claim that \`assets/plot-NN/cut-XX-clean.webp\` was created unless the
186
+ file actually exists.** Never report an image as generated when you only
187
+ produced a prompt.
188
+
189
+ ### Cover fallback (only if cover generation is unavailable)
190
+
191
+ The cover follows the same guardrail. If you cannot generate \`assets/cover.webp\`
192
+ in this session, do NOT hang on the cover: say so in one line and tell the writer
193
+ to import a cover via the OWS "Import generated image" control on the genesis
194
+ publish panel (it accepts a PNG and converts it). The same one-attempt-then-report
195
+ rule applies to a cover-only request.
196
+
197
+ ### Finishing the task: post a completion line and STOP (#311)
198
+
199
+ You are DONE once every requested cut has been handled — each one either has its
200
+ verified clean-image file saved, OR a clear blocker reported and the
201
+ prompt-and-import fallback handed off — and the cover is saved or its fallback
202
+ reported. Verifying that each file exists at the right path, format, and size is
203
+ ALL the checking required.
204
+
205
+ Do NOT start an open-ended "let me visually re-inspect the images" loop: you
206
+ cannot meaningfully re-judge generated art from a terminal, and looping there is
207
+ exactly what leaves the OWS session stuck in a long-running \`Working\` state after
208
+ the files already exist.
209
+
210
+ Post ONE final completion line, then STOP (return to the idle prompt — do not keep
211
+ working or wait for more input):
212
+
213
+ CARTOON ASSETS COMPLETE — N/N clean cut images saved, cover <saved|skipped>. Ready for lettering in OWS.
214
+
215
+ If some assets are blocked, use instead:
216
+
217
+ CARTOON ASSETS PARTIAL — X/N clean cut images saved, Y blocked: <reasons>. Ready for lettering on the saved cuts in OWS.
218
+
219
+ **Hand off generated PNGs for one-click import (#403).** Any cut whose image is a
220
+ PNG left in \`~/.codex/generated_images/…\` (the normal output of built-in
221
+ generation) must be named for the writer so they can import it — never silently
222
+ leave it in the cache, where a writer cannot find it. For each such file, list its
223
+ real source path and the cut/cover it belongs to, e.g.:
224
+
225
+ IMPORT NEEDED — cut 3: ~/.codex/generated_images/<file>.png → import into cut 3 with the OWS per-cut "Import from Codex" button (it converts the PNG automatically).
226
+ IMPORT NEEDED — cover: ~/.codex/generated_images/<file>.png → import via the genesis panel's "Import generated image" control.
227
+
228
+ A generated PNG handed off this way is a complete, expected result — list every one
229
+ so none is stranded, and do NOT re-attempt generation just to avoid the import.
230
+
231
+ ### Post-run checklist — files live in the STORY FOLDER, not the cache (#362)
232
+
233
+ Before you post the completion line, confirm in the story folder itself (not in
234
+ \`~/.codex/generated_images/\`):
235
+
236
+ - \`structure.md\`, \`genesis.md\`, and every \`plot-NN.cuts.json\` exist.
237
+ - Each cut you reported as saved has a real \`assets/plot-NN/cut-XX-clean.webp\`
238
+ (WebP/JPEG, < 1MB) at that path — verify with \`find\`/\`file\`, not from memory.
239
+ - The cover, if generated, is at \`assets/cover.webp\`.
240
+ - Any image you could NOT finalize is named in an \`IMPORT NEEDED\` line above.
241
+
242
+ If a file is only in the generation cache, it does not count as saved — either
243
+ finalize it into the asset path or report it as \`IMPORT NEEDED\`.
244
+
245
+ After that line, the lettering/export/publish steps are the writer's job in OWS —
246
+ hand off and stop.`;
247
+ }
248
+
249
+ return `### You cannot create image files yourself — hand the prompt to the writer
250
+
251
+ You (Claude) do **not** generate image files in this terminal. You produce the
252
+ exact clean-image PROMPT for each cut; the writer (or a configured image tool, if
253
+ any) generates the actual image externally and imports it back into OWS.
254
+
255
+ If no image-generation tool is available in the terminal, for each cut:
256
+ 1. PREPARE THE EXACT CLEAN-IMAGE PROMPT (shot type + description + characters +
257
+ the no-text constraint). This is the same prompt the OWS UI exposes per cut.
258
+ 2. Tell the writer to generate it externally (any provider/tool they prefer) and
259
+ then upload/import the resulting WebP/JPEG into OWS using the per-cut
260
+ "Copy prompt" + "Upload clean image" controls.
261
+ 3. **Do NOT claim that \`assets/plot-NN/cut-XX-clean.webp\` was created unless the
262
+ file actually exists.** Never report an image as generated when you only
263
+ produced a prompt.
264
+ 4. After a clean image file exists, update/verify the cut's \`cleanImagePath\`
265
+ through the OWS import flow or the cuts API — do not invent the path.
266
+
267
+ If a configured image tool exists, it may generate the clean image directly;
268
+ otherwise the manual prompt + import path above is the safe baseline.
269
+
270
+ Saved clean images live at: \`assets/plot-NN/cut-XX-clean.webp\` (OWS records the
271
+ path; you do not hand-write it unless the file is really there).
272
+
273
+ **Only exception:** Include text in images when it is part of the physical scene
274
+ (a sign on a building, text on a screen, a letter being read) AND the writer
275
+ has explicitly requested it.`;
276
+ }
277
+
278
+ /** Provider-branched step 2 of the Episode Workflow checklist. */
279
+ function episodeWorkflowImageStep(provider: AgentProvider): string {
280
+ if (provider === "codex") {
281
+ return `2. **Generate** — Write the planning files first. Create the real clean-image file for each cut.
282
+ If it is WebP/JPEG under 1MB, finalize it into \`assets/plot-NN/cut-XX-clean.webp\`;
283
+ if it is a PNG (the usual built-in output), leave it in the cache and hand it off
284
+ with an \`IMPORT NEEDED\` line for the writer's per-cut "Import from Codex" button —
285
+ both are acceptable finishes. Checkpoint per file and make one bounded attempt; if
286
+ image generation is unavailable, report the blocker and fall back to preparing the
287
+ prompt for the writer to import — never sit in an indefinite \`Working\` state.`;
288
+ }
289
+ return `2. **Prompt & import** — Prepare the clean-image prompt for each cut; the writer
290
+ generates it externally (or a configured image tool, if any) and uploads/
291
+ imports the clean image via OWS. You do not create the image file yourself.`;
292
+ }
293
+
294
+ function cartoonInstructions(provider: AgentProvider): string {
295
+ return `# Writing Instructions — Cartoon
296
+
297
+ > Auto-generated by PlotLink OWS. Do not edit manually.
298
+ > For API endpoints and publish details, see ~/.plotlink-ows/CLAUDE.md.
299
+
300
+ ## Story File Structure
301
+
302
+ \`\`\`
303
+ .story.json — { "contentType": "cartoon" }
304
+ structure.md — Story bible: style guide, character bible, episode format (outline, not publishable)
305
+ genesis.md — Reader-facing prologue / Episode 1 opening (prose, real # title)
306
+ genesis.cuts.json — Cut plan for Genesis-as-Episode-1 ({ "plotFile": "genesis", cuts: [...] })
307
+ plot-NN.cuts.json — Cut plan for episode NN
308
+ plot-NN.md — Episode publish markdown (image sequence, generated from final uploaded images)
309
+ assets/
310
+ plot-NN/ — (and assets/genesis/ for Genesis cuts)
311
+ cut-01-clean.webp — Clean image (no text or bubbles)
312
+ cut-01-final.webp — Final lettered version
313
+ ...
314
+ \`\`\`
315
+
316
+ ### Scaffold states (keep these explicit)
317
+
318
+ A new cartoon scaffold has four distinguishable states — don't let a placeholder
319
+ look publish-ready:
320
+
321
+ - **Story bible / outline** — \`structure.md\`. Never publishable; it's the plan.
322
+ - **Episode 1 planned** — \`genesis.md\` (prose opening) + \`genesis.cuts.json\`
323
+ with real cuts. Use \`"plotFile": "genesis"\` — OWS supports Genesis as
324
+ Episode 1, not only \`plot-NN\`.
325
+ - **Future episode placeholder** — a \`plot-NN.cuts.json\` with \`"cuts": []\` (and
326
+ a stub \`plot-NN.md\`) is a **planned / not-started** episode. Leave \`cuts\`
327
+ empty until you actually plan that episode; OWS shows it as "not started", not
328
+ "ready to publish". Do NOT hand-write publish markdown for it.
329
+ - **Publish markdown** — \`plot-NN.md\` is generated by OWS from the final
330
+ uploaded cut images (the "Prepare episode for publish" step), never authored by
331
+ hand.
332
+
333
+ ## structure.md Format (Cartoon)
334
+
335
+ ### Required Sections
336
+
337
+ 1. **Visual Style Guide**
338
+ - Art style (manga, Franco-Belgian, webcomic, American comic, etc.)
339
+ - Color palette (full color, limited palette, monochrome + spot color)
340
+ - Line weight and inking style
341
+ - Panel/cut layout preferences (grid, dynamic, full-bleed)
342
+ - **Style Lock (REQUIRED — prevents photoreal drift).** Write one short,
343
+ reusable block that locks the rendering style and you repeat on EVERY image
344
+ generation. It MUST carry both positive descriptors and hard negatives,
345
+ because image generation drifts into polished digital painting / photoreal
346
+ concept art unless the prompt fights it. Pattern (adapt the specifics to this
347
+ story's reference, e.g. a semi-realistic Korean webtoon):
348
+ - **Positive:** illustrated comic/webtoon panel art; clean black contour/ink
349
+ lines; flat or cel shading; simplified-but-realistic (semi-realistic)
350
+ anatomy and faces; backgrounds drawn as illustrated panels, not photos.
351
+ - **Hard negatives:** NOT photorealistic, NOT a photograph, NOT a glossy or
352
+ painterly digital painting, NOT concept art, NOT a 3D/CGI render, NOT
353
+ airbrushed, no photoreal textures.
354
+ "Semi-realistic webtoon" on its own is too weak — keep the explicit negatives.
355
+ OWS already prepends a baseline style lock to every per-cut "Copy Codex task" /
356
+ "Copy prompt"; your Style Lock here adds the story-specific reference on top and
357
+ must be honored on the cover too.
358
+
359
+ 2. **Character Bible**
360
+ For each character, describe their VISUAL identity:
361
+ - Physical features: hair color/style, eye color, build, height
362
+ - Signature outfit and accessories
363
+ - Distinguishing visual features (scars, glasses, tattoos)
364
+ - Expression notes (resting expression, characteristic gestures)
365
+
366
+ 3. **Episode Format**
367
+ - Target cuts per episode (typical: 4–12)
368
+ - Pacing rhythm: how to alternate wide/medium/close-up shots
369
+ - Aspect ratio preference for cuts
370
+
371
+ 4. **Bubble and Lettering Conventions**
372
+ - Speech bubble style (round, angular, cloud-like)
373
+ - Thought bubble style
374
+ - Narration/caption box style
375
+ - Sound effect (SFX) conventions
376
+ - Font or lettering style preferences
377
+
378
+ 5. **Cut Planning Rules**
379
+ - Shot progression guidelines (e.g., establish → act → react)
380
+ - When to use wide vs. close-up shots
381
+ - Transition conventions between scenes
382
+
383
+ 6. **Genesis Opening Plan (Reader Onboarding)**
384
+ - How Genesis opens the story for readers: the lead's situation and their
385
+ emotional/comedic problem, the tone, and what's at stake.
386
+ - The bridge from Genesis into episode 1 — what plot-01 picks up, so the
387
+ prologue and the first episode connect without restarting or repeating.
388
+ - Keep it text-only for MVP; note the intended title.
389
+
390
+ ## genesis.md Format — Reader-facing Prologue / Story Opening
391
+
392
+ On PlotLink the story page **opens at Genesis**, so for a cartoon write \`genesis.md\`
393
+ as the actual **prologue / story opening readers see first** — NOT a back-cover
394
+ synopsis, and NOT a cold jump into scene 1. It is text-only for this MVP (no
395
+ image cuts) and **must start with a real \`# Title\` heading** (the published
396
+ chapter title).
397
+
398
+ Write it as polished reader-facing prose (~600–1000 characters) across **3–6
399
+ short paragraphs** that build — do NOT compress the opening into a single block
400
+ or a one-line premise. Let the prologue breathe across these beats:
401
+
402
+ - **Opens on the main character's situation** and their emotional or comedic
403
+ problem — put us in a moment, not a pitch.
404
+ - **What the lead wants** (their desire/goal) and **what's at stake** — give the
405
+ reader someone to root for before the first episode.
406
+ - **Establishes tone and premise** — the comedic/romantic hook that makes this
407
+ story this story.
408
+ - **Builds toward the first visual episode** and ends on a beat that hands off
409
+ into Episode 01 — a clear bridge, so plot-01 feels like the story continuing,
410
+ not starting over.
411
+ - **Avoids lore dumps** and worldbuilding exposition — reveal through the
412
+ character's immediate situation.
413
+ - For comedic romance / webtoon pacing: lead with the funny/awkward hook and the
414
+ relationship tension; keep it warm and propulsive.
415
+
416
+ A single dense paragraph or a bare logline is NOT enough — readers meet Genesis
417
+ first, so it must onboard them with real buildup, not drop them cold into a scene.
418
+
419
+ Genesis vs plot-01: **Genesis opens and onboards the reader** (prose prologue);
420
+ **plot-01 is the first cut-based visual episode** that must **open on a titled
421
+ beat continuing directly from where Genesis leaves off** — not a cold jump and
422
+ not a restart. Do not start plot-01 without the setup Genesis provides, and do
423
+ not duplicate the prologue inside plot-01.
424
+
425
+ ## Cut Planning — plot-NN.cuts.json
426
+
427
+ Before generating images for an episode, create the cut plan first. The cuts.json
428
+ is the single source of truth for the episode's visual sequence.
429
+
430
+ **CRITICAL: Use this EXACT schema. Do NOT invent alternate or nested planning
431
+ structures.** OWS reads this file with a strict validator — any other shape is
432
+ rejected.
433
+
434
+ Valid \`plot-01.cuts.json\`:
435
+
436
+ \`\`\`json
437
+ {
438
+ "version": 1,
439
+ "plotFile": "plot-01",
440
+ "title": "Episode 1 — First Rain",
441
+ "cuts": [
442
+ {
443
+ "id": 1,
444
+ "shotType": "wide",
445
+ "description": "Establishing shot of the rain-soaked city at dusk",
446
+ "characters": ["Mira"],
447
+ "dialogue": [
448
+ { "speaker": "Mira", "text": "It always rains here." }
449
+ ],
450
+ "narration": "The city never slept, and neither did she.",
451
+ "sfx": "RAIN",
452
+ "cleanImagePath": null,
453
+ "finalImagePath": null,
454
+ "exportedAt": null,
455
+ "uploadedCid": null,
456
+ "uploadedUrl": null,
457
+ "overlays": []
458
+ }
459
+ ]
460
+ }
461
+ \`\`\`
462
+
463
+ **Always set a top-level \`title\`** — a real, **reader-facing, specific** episode
464
+ title (e.g. "Episode 1 — First Rain", or just "First Rain"). The published
465
+ cartoon markdown is image-only with no heading, so this \`title\` becomes the
466
+ **public chapter title readers see on PlotLink**. It must NOT be a default or
467
+ placeholder label: never the raw filename \`plot-01\`, and never a bare generic
468
+ "Episode NN"/"Chapter NN". OWS **blocks** publish on those (#347/#358/#365/#368)
469
+ and, after publish, **verifies the indexed public title** is reader-facing
470
+ (#379). The same rule applies to the story itself — **Genesis must carry a real
471
+ \`# Title\`, never the default \`Genesis\` label**. EVERY episode needs a public
472
+ title specific to what happens in it, not a numbered placeholder.
473
+
474
+ ### Text / interstitial panels (#352)
475
+
476
+ A cut may be a **text panel** instead of an image cut — a narration card, title
477
+ card, time/scene transition ("Three weeks later"), or a beat of pure prose for
478
+ pacing/buildup. Set \`"kind": "text"\` on the cut (image cuts omit \`kind\` or use
479
+ \`"image"\`). A text panel needs **no clean image**: it renders text on a styled
480
+ background and still exports + uploads a final image like any cut.
481
+
482
+ \`\`\`json
483
+ { "id": 4, "kind": "text", "shotType": "wide", "description": "Time skip",
484
+ "background": "#101820", "aspectRatio": "4:5",
485
+ "characters": [], "dialogue": [], "narration": "Three weeks later.", "sfx": "",
486
+ "cleanImagePath": null, "finalImagePath": null, "exportedAt": null,
487
+ "uploadedCid": null, "uploadedUrl": null, "overlays": [] }
488
+ \`\`\`
489
+
490
+ Use text panels **between image cuts** to control rhythm — an opening title, a
491
+ scene/time transition, an interior-monologue beat, or buildup before a reveal.
492
+ Optional \`background\` (CSS color) and \`aspectRatio\` ("W:H") style the card. In
493
+ OWS the writer can also add one with the **"Add text panel"** button, then open
494
+ the lettering editor to place the text. Don't overuse them; they punctuate the
495
+ visual story, they don't replace it.
496
+
497
+ ### Required field naming (do NOT use the wrong forms)
498
+
499
+ | Use this | NOT this |
500
+ |----------|----------|
501
+ | \`version: 1\` (top level) | \`$schema\`, \`story\`, \`workflow\`, \`promptDefaults\` |
502
+ | numeric \`id\` (1, 2, 3) | string ids like \`"c01"\` |
503
+ | \`shotType\` | \`shot\` |
504
+ | \`description\` | nested \`image.prompt\` |
505
+ | \`dialogue[].text\` | \`dialogue[].line\` |
506
+ | \`narration\` (string) | nested \`text.narration\` |
507
+ | \`sfx\` (single string) | \`sfx\` as an array |
508
+ | \`cleanImagePath\` / \`finalImagePath\` (top-level on the cut) | nested \`image.clean\` / \`image.final\` |
509
+
510
+ - \`shotType\` must be one of: \`wide\`, \`medium\`, \`close-up\`, \`extreme-close-up\`.
511
+ - All image/export/upload path fields start as \`null\`; OWS fills them in.
512
+ - \`overlays\` starts as an empty array \`[]\`; the lettering editor populates it.
513
+ **Leave \`overlays\` empty (\`[]\`)** — the writer places bubbles/captions in the
514
+ OWS lettering editor. If you ever do emit overlays, each MUST use the real OWS
515
+ overlay schema below — NEVER a semantic \`position\` string like \`"upper-left"\`,
516
+ which has no geometry, does not render, and would export a blank, unlettered
517
+ image.
518
+ - **Overlay schema (only if you emit overlays):** each overlay is an object with
519
+ \`id\` (string), \`type\` (\`"speech"\` | \`"narration"\` | \`"sfx"\`), numeric \`x\`, \`y\`,
520
+ \`width\`, \`height\` (fractions of the image, 0–1, where \`x\`/\`y\` are the top-left
521
+ corner), \`text\` (string), optional \`speaker\` (speech only), and optional
522
+ \`tailAnchor\` (\`{ "x": number, "y": number }\`, speech only). Example:
523
+ \`{ "id": "ov-1", "type": "speech", "x": 0.08, "y": 0.06, "width": 0.4, "height": 0.16, "text": "...", "speaker": "Hana", "tailAnchor": { "x": 0.5, "y": 1.2 } }\`.
524
+ There is NO \`position\` field.
525
+ - **Every publishable cut must become a final uploaded image.** Even
526
+ narration-only or background-only cuts must get a clean image, be lettered/
527
+ exported, and uploaded before the episode can publish. \`cleanImagePath\` may
528
+ be \`null\` during early planning, but a cut with no uploaded image will block
529
+ publish readiness.
530
+
531
+ ## Asset Tooling — use OWS flows, do NOT shell out to image tools
532
+
533
+ OWS owns the cartoon asset pipeline. Local image tools are NOT part of the
534
+ contract and may be absent — do **NOT** call ImageMagick (\`magick\`/\`convert\`/
535
+ \`identify\`), \`sharp\`, Playwright / headless browsers, or any ad-hoc shell tool to
536
+ resize, convert, composite, letter, or measure images. If you find yourself
537
+ reaching for a CLI image tool, STOP — there is a supported OWS action for it, and
538
+ guessing at unavailable tooling is exactly what stalls a cartoon episode.
539
+
540
+ **Deterministic handoff — who does what:**
541
+
542
+ | Step | Who | How (no shell image tools) |
543
+ |------|-----|----------------------------|
544
+ | Generate clean cut images | You (Codex) | Generate the image. If it is WebP/JPEG < 1MB, finalize it into \`assets/plot-NN/cut-XX-clean.webp\` with a plain \`cp\`/\`mv\`. If it is a PNG (the usual built-in output), do NOT convert or rename it — hand it off with an \`IMPORT NEEDED\` line for the writer's "Import from Codex" button, which converts it automatically. Either is a valid finish; do NOT post-process with magick/sharp. |
545
+ | Generate the cover | You (Codex) | Save \`assets/cover.webp\` (~600x900, WebP, < 1MB). OWS auto-detects it for genesis publish — no manual selection needed. |
546
+ | Discover / record clean images | OWS | Run the "Sync clean images" action (or let OWS auto-detect); OWS records \`cleanImagePath\`. Never hand-write paths or stat files yourself. |
547
+ | Letter & export final images | The writer, in the OWS lettering editor | Speech bubbles, captions, and SFX are placed in the OWS editor and exported to \`assets/plot-NN/cut-XX-final.webp\`. You do NOT composite or letter text — not with magick, not with sharp, not at all. |
548
+ | Upload finals + build markdown | OWS | The writer runs "Upload & Prepare for Publish"; OWS uploads each final image and emits the publish markdown. You never hand-write \`plot-NN.md\`. |
549
+
550
+ **No agent-side image tools are required** — OWS provides image generation (your
551
+ session), clean-image sync, the lettering/export editor, and upload/markdown
552
+ generation. If a capability you genuinely need is missing (e.g. image generation
553
+ itself is unavailable in this session), do NOT improvise with shell tools: fall
554
+ back to the prompt-and-import handoff below and tell the writer, so the missing
555
+ capability surfaces immediately instead of after a dead-end path.
556
+
557
+ ## CRITICAL: Clean-Image-First Workflow
558
+
559
+ **Do NOT bake dialogue, speech bubbles, sound effects, or any text into generated images.**
560
+
561
+ Clean images must contain:
562
+ - No speech bubbles
563
+ - No text overlays
564
+ - No sound effect text
565
+ - No narration captions
566
+ - No lettering of any kind
567
+
568
+ **Lock the style on every generation — do not drift to photoreal.** Reuse the
569
+ **Style Lock** from structure.md's Visual Style Guide in EVERY image prompt (cuts
570
+ and cover) so the look stays consistent and illustrated. The requested style is an
571
+ illustrated comic/webtoon panel — clean black contour lines, flat/cel shading,
572
+ simplified-but-realistic anatomy — and explicitly **NOT** photorealistic, **NOT** a
573
+ painterly digital painting / concept art, and **NOT** a 3D render. If a generated
574
+ image reads as a photo or polished concept art, it is off-style: regenerate it as
575
+ illustrated panel art rather than accepting the drift. (The per-cut "Copy Codex
576
+ task" / "Copy prompt" OWS exposes already carries this baseline style lock.)
577
+
578
+ ${cleanImageWorkflowSection(provider)}
579
+
580
+ ## Character Consistency
581
+
582
+ - Reference the character bible from structure.md for EVERY image generation
583
+ - Maintain consistent visual traits across all cuts and all episodes
584
+ - Same hair, same eye color, same outfit unless the story dictates a change
585
+ - Note any intentional appearance changes in the cuts.json
586
+
587
+ ## Lettering Handoff
588
+
589
+ After clean images are generated and approved by the writer:
590
+
591
+ 1. The writer uses the OWS lettering editor to add speech bubbles and text
592
+ 2. Lettered versions are saved as: \`assets/plot-NN/cut-XX-final.webp\` (the OWS
593
+ editor's export writes this file — you do not create or composite it)
594
+ 3. Do NOT attempt to add bubbles or text to images — only the writer controls
595
+ lettering, via the OWS editor. Never composite text with \`magick\`, \`sharp\`,
596
+ Playwright, or any other tool; lettering/export is not an agent shell task.
597
+ 4. The publish markdown references final lettered images, not clean images
598
+
599
+ ## Publish Markdown — plot-NN.md
600
+
601
+ **Do NOT hand-write plot-NN.md with image links.** OWS generates the publish
602
+ markdown from cuts.json after final images are uploaded. Hand-authored markdown
603
+ with local asset paths (\`assets/...\`) or placeholder URLs ("final image
604
+ pending") will be rejected by publish readiness checks.
605
+
606
+ Correct flow:
607
+
608
+ 1. Upload final (lettered) images via OWS — this records the IPFS URL per cut in
609
+ cuts.json (\`uploadedUrl\`).
610
+ 2. Use OWS "Prepare episode for publish" / "Upload & Prepare for Publish" to produce
611
+ plot-NN.md. OWS emits marker-delimited blocks:
612
+ \`\`\`
613
+ <!-- ows:cartoon-cut cut-001 start -->
614
+ ![Cut 1 — Scene description](https://ipfs-gateway/...)
615
+ <!-- ows:cartoon-cut cut-001 end -->
616
+ \`\`\`
617
+ 3. Cuts that are not yet uploaded produce a safe \`<!-- ... awaiting upload -->\`
618
+ comment, never a fake image URL. Publish is blocked until all cuts upload.
619
+ 4. Each image is WebP or JPEG, under 1MB. Total markdown under 10K characters.
620
+ 5. Keep human-readable planning notes in cuts.json or structure.md — never as
621
+ fake publish image links.
622
+
623
+ ## Publishing Rules
624
+
625
+ - Content is **immutable after publish** — verify all images in Preview first
626
+ - Genesis publishes via \`createStoryline\` (one-time, text-only synopsis)
627
+ - Episodes publish via \`chainPlot\` (per episode, image-sequence markdown)
628
+ - Once published, images cannot be replaced or edited
629
+
630
+ ## Security — never print secrets into the terminal
631
+
632
+ - Do NOT print, \`echo\`, \`cat\`, or log auth material in this terminal: no
633
+ \`Authorization: Bearer\` headers or session tokens, no \`OWS_PASSPHRASE\`, and no
634
+ login passphrase or any login command that contains it.
635
+ - The app stores the passphrase in \`~/.plotlink-ows/.env\` and authenticates for
636
+ you — you never need to read or echo it. Use the documented API; keep secrets
637
+ out of command output.
638
+
639
+ ## Episode Workflow
640
+
641
+ 1. **Plan** — Create plot-NN.cuts.json with shot-by-shot breakdown
642
+ ${episodeWorkflowImageStep(provider)}
643
+ 3. **Review** — Writer reviews clean images, requests adjustments
644
+ 4. **Letter** — Writer adds speech bubbles and text via lettering editor
645
+ 5. **Upload** — Upload final lettered images to get IPFS URLs (recorded in cuts.json)
646
+ 6. **Generate** — Use OWS to generate plot-NN.md from cuts.json (do not hand-write it)
647
+ 7. **Preview** — Verify all images render correctly
648
+ 8. **Publish** — Chain the episode (immutable after this step)
649
+
650
+ ## Final response format — end EVERY completed task with this (#419)
651
+
652
+ A validation summary alone is not enough: a non-technical writer needs to know
653
+ exactly what to do next and what to paste back to you. So after ANY major task
654
+ (story setup, episode planning, clean-image generation, lettering handoff,
655
+ export, upload, publish), end your reply with this exact five-part section, in
656
+ plain language (no jargon, no internal file paths in the pasteable prompt):
657
+
658
+ \`\`\`
659
+ Done
660
+ - <3-5 short bullets of what changed>
661
+
662
+ Current stage
663
+ - <one line, e.g. "Episode 1 planned; clean images not generated yet">
664
+
665
+ Next recommended action
666
+ - <one clear action in plain language>
667
+
668
+ Prompt you can paste next
669
+ - <a single copy-paste prompt the writer can hand back to you>
670
+
671
+ Do not do yet
672
+ - <short safeguards when relevant; omit the bullet if nothing applies>
673
+ \`\`\`
674
+
675
+ Pick the "Prompt you can paste next" to match the stage you just finished:
676
+
677
+ - **After story setup** → \`Plan the cuts for Episode 1 / Genesis. Don't generate images, letter, upload, or publish yet.\`
678
+ - **After episode planning** → \`Generate clean images for Episode 1 / Genesis from genesis.cuts.json. Do not letter, upload, or publish yet.\`
679
+ - **After clean images** → \`The clean images for Episode 1 are ready — review them, then I'll letter them in the OWS editor.\` (lettering/export/upload happen in the OWS UI, not via the agent)
680
+ - **After lettering/export/upload** → \`Episode 1's final images are uploaded — prepare the publish markdown and show me the Preview before publishing.\`
681
+ - **After publish** → \`Verify Episode 1 is live on plotlink.xyz, then let's plan Episode 2.\`
682
+
683
+ Keep it concise — this section replaces a long technical status dump, it does not
684
+ add to one. Write the whole section in the writer's language. This format is
685
+ required for cartoon stories regardless of which assistant (Claude or Codex) you
686
+ are; it never applies to fiction stories.
687
+ `;
688
+ }
689
+
690
+ export function generateStoryInstructions(
691
+ contentType: "fiction" | "cartoon",
692
+ // Cartoon instructions branch by provider so a Codex cartoon session is told to
693
+ // create the real clean-image file, while Claude/legacy sessions get the manual
694
+ // prompt-and-import handoff. Fiction ignores provider (always Claude). Absent ⇒
695
+ // "claude" (matches the fiction-safe absent⇒Claude default; see #268).
696
+ provider: AgentProvider = "claude",
697
+ ): string {
698
+ if (contentType === "cartoon") return cartoonInstructions(provider);
699
+ return fictionInstructions();
700
+ }
701
+
702
+ const MARKER_PREFIX = "<!-- plotlink-ows:story-instructions:";
703
+
704
+ // Cartoon markers encode the provider so a story repaired Claude⇒Codex (or vice
705
+ // versa) regenerates its CLAUDE.md on the next spawn instead of keeping stale,
706
+ // provider-mismatched wording. Fiction has no provider variant — its marker is
707
+ // unchanged for rollback safety.
708
+ function marker(contentType: "fiction" | "cartoon", provider: AgentProvider): string {
709
+ const suffix = contentType === "cartoon" ? `cartoon:${provider}` : "fiction";
710
+ return `${MARKER_PREFIX}${suffix} -->`;
711
+ }
712
+
713
+ export function writeStoryInstructions(
714
+ storyDir: string,
715
+ contentType: "fiction" | "cartoon",
716
+ provider: AgentProvider = "claude",
717
+ ): void {
718
+ const claudeMdPath = path.join(storyDir, "CLAUDE.md");
719
+ const expectedMarker = marker(contentType, provider);
720
+
721
+ if (fs.existsSync(claudeMdPath)) {
722
+ try {
723
+ const firstLine = fs.readFileSync(claudeMdPath, "utf-8").split("\n")[0];
724
+ if (firstLine === expectedMarker) return;
725
+ if (!firstLine.startsWith(MARKER_PREFIX)) return;
726
+ } catch { /* regenerate on error */ }
727
+ }
728
+
729
+ const content = expectedMarker + "\n" + generateStoryInstructions(contentType, provider);
730
+ fs.writeFileSync(claudeMdPath, content, "utf-8");
731
+ }