Skip to content

feat(pipeline): length profile, per-stage gen config, comic front cover#233

Merged
atomantic merged 19 commits into
mainfrom
feat/pipeline-genconfig-length-cover
May 15, 2026
Merged

feat(pipeline): length profile, per-stage gen config, comic front cover#233
atomantic merged 19 commits into
mainfrom
feat/pipeline-genconfig-length-cover

Conversation

@atomantic
Copy link
Copy Markdown
Owner

Summary

Cherry-picks three additive pipeline-UX features from a pre-crash recovery branch into main, integrated with main's current Nouns / SeriesLlmPicker / universeBuilder direction (the recovery branch's removals of those are NOT applied here).

1. Per-issue length profile picker

  • New picker in the Pipeline Issue header — teaser (8pg/10min), standard (22pg/24min, default), extended (32pg/36min), finale (44pg/48min), or custom (free-form pages + minutes).
  • Drives prompt-template size targets via new {{lengthTargets.*}} variables (`profile`, `pageTarget`, `minutesTarget`, `proseWordsMin/Max`, `beatsMin/Max`). All five text-stage prompts (idea, prose, comic-script, tv-script, season-episodes) now consume these instead of hardcoded "22 pages / 24 minutes".
  • Season-episodes generator emits a `lengthProfile` per episode so a finale auto-scales without manual tweaking.
  • New `server/lib/issueLength.js` is the source of truth; `client/src/lib/issueLength.js` is a manual mirror following the existing repo convention (`runnerFamilies.js`, `scenePrompt.js`, etc.).

2. Per-stage generation settings (gear icon)

  • New gear icon in the issue header on the Storyboards tab opens a modal with:
    • Image backend (`auto` / `local` / `codex`) — Auto follows the server resolver, which now defaults to codex when `imageGen.codex.enabled` (still falls back to local diffusion otherwise).
    • Local image model picker (only shown for `local` mode).
    • AI-refine LLM override (provider + model) for the `AI: refine` buttons.
  • Persisted on `stages..genConfig` so reloads keep the user's choice.
  • The Comic editor (`comicScript` tab) keeps its existing image-gen drawer — the gear icon stays off that tab to avoid duplicating controls.

3. Comic-issue front cover

  • New cover card in the Comic editor lets the user write a cover concept (mood / subject / framing) and click Render cover. Server composes the full prompt server-side (series masthead + issue-number tag + concept) so the user only owns the creative text.
  • Persisted on `stages.comicPages.cover = { script, imageJobId, prompt }`.
  • New `POST /pipeline/issues/:id/stages/comicPages/cover/render` route + `generatePipelineComicCover()` client helper.
  • The comic-script parser now also recognizes an optional `## Cover concept` section in the LLM output and the extract-pages route auto-seeds a blank `cover.script` from it (preserves any user edit).

Parser improvements

  • `server/lib/comicScriptParser.js` now accepts the simpler `Panel N` / `Field:` plain-line format the updated comic-script prompt emits, alongside the legacy `### Panel N` / `Field:` form. Returns `{ coverConcept, pages: [{ rawText, panels }] }` — keeps the existing `rawText` field name so persisted issue data continues to render.

Conflicts not applied

The recovery branch also removed Nouns, SeriesLlmPicker, refinePipelineCharacter, resolvePipelineArcIssues, updatePipelineComicPage, and renamed universeBuilder → worldBuilder. None of those are in this PR — main has actively kept developing the universeBuilder + Nouns / Universe Canon Phase B work since the recovery branch was created (2026-05-14), and main has gone the opposite direction on naming. Only the additive features were cherry-picked.

Test plan

  • Server suite — 4556 tests pass (5 skipped, pre-existing)
  • `comicScriptParser.test.js` — 13 tests including 5 new ones for cover + plain format
  • Client `npm run build` clean
  • Client `npm run lint` — 0 errors (30 warnings, all pre-existing in unrelated files)
  • Manual: load an issue, switch length profile, run idea/prose stages, verify prompt picked up the new size targets
  • Manual: on Storyboards tab, open gear icon, switch image backend to codex, render — verify dispatch
  • Manual: on Comic tab, write a cover concept, click Render cover, verify masthead + issue number appear on the rendered cover

🤖 Generated with Claude Code

atomantic added 2 commits May 14, 2026 15:51
Three additive pipeline-UX features rescued from a pre-crash recovery
branch and cherry-picked into main:

- Length profile picker in the issue header (teaser / standard /
  extended / finale / custom). Drives prompt-template size targets via
  {{lengthTargets.*}} so beat counts, prose word ranges, and page
  counts scale with the profile instead of hardcoded 22pg/24min.
  Season-episodes generator emits a lengthProfile per episode.

- Per-stage generation settings modal (gear icon) on visual stages.
  Sets imageMode (auto/local/codex), pinned local image model, and a
  refine-LLM override. Persisted on stages.<stageId>.genConfig and
  threaded through generate + refine APIs. visualStages.resolveMode
  now defaults to codex when imageGen.codex.enabled.

- Comic-issue front cover. Optional cover concept on
  stages.comicPages.cover, new POST /comicPages/cover/render route
  that composes a cover-art prompt (series masthead + issue-number
  tag + concept) and enqueues an image-gen job.

- Comic-script parser now accepts an optional ## Cover concept section
  plus the simpler "Panel N" / "Field:" plain-line format alongside
  the legacy ### Panel / **Field:** form.

All server tests pass (4556), client build + lint clean.
ComicPagesStage is unreachable in the running app — PipelineIssue
redirects /comicPages URLs to /comicScript where ComicScriptStage owns
the merged page editor. The cover UI initially placed in ComicPagesStage
never rendered for users. Moves the cover card into ComicScriptStage,
backed by the same stages.comicPages.cover record, and uses the
existing renderOpts so it picks up the user's image-gen drawer choice.

Also: extract-pages now seeds a blank cover.script from the LLM's
parsed `## Cover concept` section (preserves any user-edited cover);
drop unused getProfileLabels export; drop dead `comicPages` key from
VISUAL_STAGE_LABELS (stageId is never that value).
Copilot AI review requested due to automatic review settings May 14, 2026 22:58
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds pipeline UX controls for issue length, per-stage visual generation settings, and comic cover rendering, with server-side prompt/render support and parser updates.

Changes:

  • Adds issue length profiles and injects computed targets into pipeline prompt contexts/templates.
  • Adds visual generation settings UI/helpers and threads image/refine overrides into storyboard/comic render calls.
  • Adds comic cover concept parsing, persistence, API rendering, and UI controls.

Reviewed changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated 13 comments.

Show a summary per file
File Description
server/services/pipeline/visualStages.js Adds cover prompt composition/enqueue and Codex-preferred visual mode fallback.
server/services/pipeline/textStages.js Adds length target context for text-stage prompt rendering.
server/services/pipeline/issues.js Persists length profile, genConfig, and cover metadata on issues/stages.
server/services/pipeline/arcPlanner.js Shapes season episode length profiles.
server/routes/pipeline.js Adds schemas/routes for length fields, cover render, and cover extraction seeding.
server/lib/issueLength.js Defines server length profiles and target derivation.
server/lib/comicScriptParser.js Adds cover concept and plain-format panel parsing.
server/lib/comicScriptParser.test.js Updates parser expectations and adds cover/plain-format tests.
data.sample/prompts/stages/pipeline-idea-expansion.md Uses length targets in idea prompt.
data.sample/prompts/stages/pipeline-prose.md Uses length targets in prose prompt.
data.sample/prompts/stages/pipeline-comic-script.md Uses page target and plain panel format in comic prompt.
data.sample/prompts/stages/pipeline-tv-script.md Uses runtime target in TV prompt.
data.sample/prompts/stages/pipeline-season-episodes.md Requests per-episode length profiles.
client/src/services/apiPipeline.js Adds comic cover render API helper.
client/src/pages/PipelineIssue.jsx Adds length picker and visual settings modal entry point.
client/src/lib/issueLength.js Adds client-side length profile display helpers.
client/src/components/pipeline/LengthProfilePicker.jsx Adds header dropdown for preset/custom length selection.
client/src/components/pipeline/stages/VisualGenSettings.jsx Adds reusable visual generation settings panel and option mappers.
client/src/components/pipeline/stages/StoryboardsStage.jsx Threads visual genConfig into storyboard render/refine actions.
client/src/components/pipeline/stages/ComicScriptStage.jsx Adds cover concept UI and cover render action.
client/src/components/pipeline/stages/ComicPagesStage.jsx Threads genConfig into comic page/panel render/refine actions.
.changelog/NEXT.md Documents the added pipeline features.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread client/src/pages/PipelineIssue.jsx
Comment thread client/src/components/pipeline/stages/ComicScriptStage.jsx Outdated
Comment thread server/routes/pipeline.js
Comment thread server/lib/issueLength.js
Comment thread server/lib/comicScriptParser.js Outdated
Comment thread server/routes/pipeline.js
Comment thread client/src/components/pipeline/LengthProfilePicker.jsx
Comment thread client/src/components/pipeline/stages/StoryboardsStage.jsx
Comment thread client/src/components/pipeline/stages/ComicPagesStage.jsx
Comment thread client/src/pages/PipelineIssue.jsx Outdated
…h bounds

- updateIssue() now per-stage-merges so partial patches like { genConfig }
  or { cover } no longer erase sibling fields like scenes / pages
- comicScriptParser PANEL_RE requires a standalone header line so
  description text mentioning "Panel N" no longer splits panels
- length-profile bounds (custom pages 4-120 / minutes 4-240) shared between
  the sanitizer, Zod schema, and client picker via exported constants
- arcPlanner season-episode generation rejects the `custom` sentinel and
  falls back to `finale` when arcRole=finale (otherwise default profile)
- comicCoverRenderSchema accepts `seed` so the shared image-gen drawer
  flows render settings into the cover render
- sanitizeVisualStage drops `cover` on non-comicPages stages, matching the
  documented contract
- LengthProfilePicker exposes aria-haspopup/aria-expanded and disables
  during auto-run so picker state stays consistent across stages
- tests: new issueLength suite, cover compose/enqueue suite, panel-regex
  regression test, per-stage merge test, cover-drop contract test,
  arcRole length fallback test
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 26 out of 26 changed files in this pull request and generated 7 comments.

Comments suppressed due to low confidence (1)

.changelog/NEXT.md:31

  • The standalone Comic Pages tab is hidden and /comicPages redirects to the merged Comic tab, so saying the cover card sits in the "Comic Pages tab" is misleading for users reading the release notes. Refer to the Comic editor/tab instead.
    user's concept) and enqueues an image-gen job; the cover card sits
    above the page list in the Comic Pages tab.

Comment thread server/services/pipeline/issues.js Outdated
Comment thread client/src/components/pipeline/stages/VisualGenSettings.jsx
Comment thread client/src/components/pipeline/stages/VisualGenSettings.jsx Outdated
Comment thread client/src/components/pipeline/stages/ComicScriptStage.jsx Outdated
Comment thread server/routes/pipeline.js Outdated
Comment thread data.sample/prompts/stages/pipeline-comic-script.md
Comment thread .changelog/NEXT.md Outdated
…de, clear stale state

- updateIssue() deep-merges `cover` and `genConfig` sub-objects so a partial
  `{ cover: { script } }` blur save preserves the imageJobId/prompt that a
  racing Render-cover mutation just persisted
- updateIssue() clears `errorMessage` when a stage transitions out of
  error/generating state, restoring the implicit-clear behavior the
  per-stage merge introduced as a regression
- ComicScriptStage cover blur now sends only `{ script }` (relies on
  server merge) — eliminates the closure-stale-cover race
- VisualGenSettings `resolveAutoLabel` mirrors server resolveMode priority
  (settings.imageGen.mode first, then codex.enabled) so the UI Auto label
  matches actual dispatch
- VisualGenSettings lookup cache clears on transient failure so a later
  mount can retry instead of being stuck with fallback null/[] forever
- extract-pages route clears stale imageJobId/prompt when seeding new
  cover.script from the parsed concept (they belonged to a different script)
- comic-script prompt now emits a `## Cover concept` section so the parser
  auto-seeding path actually fires in normal generation
- changelog scoping: gear modal is on `storyboards` only; comicScript keeps
  its own image-gen drawer
- tests: deep-merge for cover/genConfig, explicit null-clear semantics,
  error-clear on status transition, error-preserve when still in error
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 26 out of 26 changed files in this pull request and generated 5 comments.

Comment thread client/src/lib/issueLength.js
Comment thread data.sample/prompts/stages/pipeline-comic-script.md Outdated
Comment thread .changelog/NEXT.md Outdated
Comment thread server/services/pipeline/issues.js Outdated
Comment thread client/src/components/pipeline/stages/ComicScriptStage.jsx
atomantic and others added 2 commits May 14, 2026 21:52
…ig-length-cover

# Conflicts:
#	.changelog/NEXT.md
Track cover job status via MediaJobThumb's onStatus callback (same
pattern PageRow uses) so the Render cover button stays disabled across
the full enqueue → queued → in-progress → completion lifecycle, not
just during the POST request.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 15, 2026 04:55
…a fixes

- client/src/lib/issueLength.js: clampInt('') returns null per contract
  instead of coercing to 0 and clamping up to the minimum.
- data.sample/prompts/stages/pipeline-comic-script.md: align the "MUST
  start with" instruction with the shown output structure so the LLM
  doesn't drop the issue title heading.
- .changelog/NEXT.md: point the cover-card description at the Comic
  tab (where the UI actually lives) instead of the redirected-away
  Comic Pages tab.
- server/services/pipeline/issues.js: rewrite the misleading updateStage
  comment to accurately describe its shallow-merge semantics.
- server/routes/pipeline.js: schema parity — issueCreateSchema now
  accepts the new length fields (lengthProfile/pageTarget/minutesTarget)
  to match the createIssue service signature.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 26 out of 26 changed files in this pull request and generated 9 comments.

Comment thread data.sample/prompts/stages/pipeline-comic-script.md
Comment thread data.sample/prompts/stages/pipeline-prose.md Outdated
Comment thread data.sample/prompts/stages/pipeline-tv-script.md Outdated
Comment thread client/src/components/pipeline/LengthProfilePicker.jsx Outdated
Comment thread server/routes/pipeline.js
Comment thread .changelog/NEXT.md
Comment thread client/src/components/pipeline/stages/VisualGenSettings.jsx Outdated
Comment thread server/services/pipeline/visualStages.js Outdated
Comment thread server/routes/pipeline.js
atomantic and others added 2 commits May 14, 2026 22:08
…trigger

The popup is a mixed disclosure panel (preset buttons + custom inputs),
not a WAI-ARIA menu. Remove aria-haspopup="menu" — which incorrectly
advertised menu semantics to assistive tech — and keep only aria-expanded,
which correctly describes a generic disclosure control. Also add an Escape
keydown listener so the panel can be dismissed without a mouse click.

Addresses Copilot review PRRT_kwDOQx8jQ86CQynM.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ver route tests

- pipeline-prose.md: drop the conflicting "one scene per 3-5 comic pages"
  guidance — the explicit beatsMin/beatsMax range already pins the scene
  count, and the page-density phrasing implied 4-7 scenes vs. the actual
  8-12 for a 22-page standard issue.
- pipeline-tv-script.md: template the prose-source word count
  ({{lengthTargets.proseWordsMin}}-{{lengthTargets.proseWordsMax}})
  instead of hardcoding "800 words" — the prose stage now scales to
  the issue's length profile so the TV stage shouldn't undercount.
- visualStages.js: gate codex mode on `imageGen.codex.enabled` in
  resolveMode so explicit `mode: 'codex'` (per-stage or settings-level
  default) falls back to local when the toggle is off, instead of
  enqueuing a doomed job the dispatcher will reject.
- VisualGenSettings.jsx: mirror the same gate in the client preview so
  the modal's "Auto -> Codex" / "Codex (saved)" labels reflect the
  actual enabled state.
- pipeline.test.js: add enqueueComicCover to the visualStages mock and
  three integration tests for the cover render route (success +
  persistence, 404, 400 schema rejection).
Copilot AI review requested due to automatic review settings May 15, 2026 05:11
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 27 out of 27 changed files in this pull request and generated 8 comments.

Comment thread client/src/components/pipeline/stages/VisualGenSettings.jsx Outdated
Comment thread client/src/components/pipeline/stages/VisualGenSettings.jsx Outdated
Comment thread client/src/components/pipeline/stages/VisualGenSettings.jsx
Comment thread client/src/pages/PipelineIssue.jsx
Comment thread server/routes/pipeline.js
Comment thread server/services/pipeline/textStages.js
Comment thread client/src/components/pipeline/stages/VisualGenSettings.jsx Outdated
Comment thread server/services/pipeline/issues.js Outdated
…er/length test coverage

VisualGenSettings.jsx:
- Clear pinned imageModelId when switching backend to auto/codex so the
  saved genConfig stops advertising a model id that genConfigToImageOptions
  ignores. Mirrored on the server in sanitizeGenConfig.
- Rewrite the codex-disabled warning to describe the actual fallback
  behavior ("renders will fall back to local diffusion") rather than the
  old "render will fail" wording — resolveMode now falls through to local
  when codex is disabled.
- Force a fresh settings + providers fetch each time the modal mounts via
  a refreshOnMount hook flag, so toggling Codex/providers in Settings
  takes effect on the next modal open instead of waiting for a hard reload.
- Track providersLoaded separately and gate the "No enabled providers"
  empty-state message on it, so the message no longer flashes during the
  in-flight providers lookup.

PipelineIssue.jsx:
- Reset settingsOpen to false when stageId changes so the modal does not
  re-mount in an open state after a tab round-trip.

issues.js:
- Sanitizer now Math.round() fractional pageTarget/minutesTarget to match
  computeIssueTargets, so persisted values agree with rendered prompt
  context (22.7 stored as 23, not 22).

Tests:
- pipeline.test.js: extract-pages cover-seed + preserve-existing-cover
  cases for the new comicScript-driven cover seeding path.
- textStages.test.js: assert lengthTargets context shape for both a
  named non-default profile (extended) and the derived custom profile.
- issues.test.js: regression test for the rounding fix and a new case
  asserting imageModelId is cleared when imageMode is non-local.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 28 out of 28 changed files in this pull request and generated 4 comments.

Comment thread client/src/components/pipeline/stages/VisualGenSettings.jsx
Comment thread data.sample/prompts/stages/pipeline-idea-expansion.md
Comment thread server/routes/pipeline.js
Comment thread server/lib/issueLength.test.js Outdated
…-episode lengthProfile test

VisualGenSettings.jsx:
- Disable the "Override default" checkbox until providersLoaded so a click
  during the in-flight providers fetch can't dereference providers[0] and
  throw. Tooltip explains the disabled state.

data/migrations/003-update-pipeline-stage-prompts.js (new):
- Migration that updates each of the 5 affected pipeline stage prompts
  (idea, prose, comic-script, tv-script, season-episodes) to the new
  length-profile-aware version, but only when the on-disk file still
  matches the pre-feature MD5. Customized files are left alone and the
  user is warned so they can merge manually. Idempotent.

scripts/setup-data.js:
- After setup, compare data.sample/prompts/stages/*.md against the
  installed copies and emit a one-line warning when any drift exists,
  pointing the user at `npm run migrations`. Fresh installs skip the
  warning (they just got a full copy).

server/routes/pipeline.test.js:
- Existing season-episode commit:true test now uses lengthProfile:'extended'
  on one of the mocked episodes and asserts it round-trips into the
  created issue, so the mapping at pipeline.js:602 has regression cover.

server/lib/issueLength.test.js:
- Grammar nit: "A request for twice as many pages" (added missing "for").

.changelog/NEXT.md:
- New "Fixed" entry pointing users at `npm run migrations` to receive
  the updated stage prompts on existing installs.
@atomantic atomantic requested a review from Copilot May 15, 2026 05:44
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 30 out of 30 changed files in this pull request and generated 4 comments.

Comment thread client/src/components/pipeline/LengthProfilePicker.jsx Outdated
Comment thread client/src/components/pipeline/stages/ComicScriptStage.jsx
Comment thread data/migrations/003-update-pipeline-stage-prompts.js Outdated
Comment thread scripts/setup-data.js Outdated
atomantic and others added 2 commits May 14, 2026 22:57
…ove unused copyFile import

- scripts/setup-data.js: drift detection now checks only the 5 pipeline
  stage prompts managed by migration 003 (pipeline-idea-expansion.md,
  pipeline-prose.md, pipeline-comic-script.md, pipeline-tv-script.md,
  pipeline-season-episodes.md) instead of all *.md files under
  data.sample/prompts/stages/. Scanning everything produced misleading
  "pipeline stage prompt" warnings for unrelated prompts (e.g.
  cd-evaluate.md, writers-room prompts) that have no migration counterpart.
  Added a comment pointing at the migration as source of truth for the list.

- data/migrations/003-update-pipeline-stage-prompts.js: removed unused
  `copyFile` import from fs/promises — the migration only uses readFile
  and writeFile.

Addresses review threads PRRT_kwDOQx8jQ86CRP4e and PRRT_kwDOQx8jQ86CRP4Q.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…over render, scope drift warning

Migration scope (already pushed in 7ed6e10e — re-listing here for context):
- scripts/setup-data.js: drift scan now iterates an explicit list of the
  5 prompts migration 003 manages, not the full data.sample/prompts/stages
  directory, so customizing an unrelated prompt (cd-evaluate.md, writers-
  room, etc.) no longer produces a misleading "pipeline stage prompt"
  warning pointing at a migration that can't update it.
- 003-update-pipeline-stage-prompts.js: drop unused copyFile import.

This commit:
- client/src/lib/issueLength.js: export CUSTOM_PAGE_MIN/MAX and
  CUSTOM_MINUTE_MIN/MAX mirroring server/lib/issueLength.js (with a
  pointer comment), so the client picker can't drift from the server
  bounds.
- LengthProfilePicker.jsx: use the mirrored constants for both number-
  input min/max attributes and the clampInt() calls.
- ComicScriptStage.jsx: on cover-script blur, when the text changed AND
  no render is currently in flight, send imageJobId:null + prompt:null
  alongside the new script so the stale-render thumbnail isn't shown
  alongside a fresh concept. The render/blur race is preserved — an
  in-flight render still tracks to completion. No-op blurs (text
  unchanged) leave the matching render alone.
Copilot AI review requested due to automatic review settings May 15, 2026 05:58
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 30 out of 30 changed files in this pull request and generated 3 comments.

Comment thread client/src/components/pipeline/stages/ComicScriptStage.jsx Outdated
Comment thread client/src/components/pipeline/LengthProfilePicker.jsx Outdated
Comment thread client/src/components/pipeline/stages/ComicScriptStage.jsx
…custom-profile validity

ComicScriptStage.jsx:
- Split coverJobInFlight into two booleans: coverJobInFlight (gates the
  Render button; still treats 'unknown' as in-flight to avoid double-
  enqueue) and coverJobActivelyRunning (excludes 'unknown'; gates the
  stale-render clear inside persistCoverScript). After a reload, an
  edit+blur on the cover concept now correctly clears the stale image
  job rather than preserving it because status hasn't reported yet.
- Suppress the textarea's blur-save when the user clicks the Render
  cover button: a useRef tracks render-button onMouseDown, and the
  blur handler skips persistCoverScript when that flag is set. The
  render handler already sends draftCoverScript, so no edit is lost.
  Prevents the blur-fires-before-click race that could erase a
  freshly-queued imageJobId by sending {imageJobId: null} after the
  render route persisted the new job.

LengthProfilePicker.jsx:
- Disable the Apply button when the custom-mode page or minute input
  is empty/non-numeric (clampInt returns null). Adds a "fill both
  fields" tooltip. Previously the user could save lengthProfile:
  'custom' with null pageTarget/minutesTarget, which the server then
  rendered with standard fallback targets — a silent miss of the
  user's intent. Reuses the single clampInt call for both the gate
  and the save payload.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 30 out of 30 changed files in this pull request and generated 2 comments.

Comment thread client/src/components/pipeline/stages/ComicScriptStage.jsx Outdated
Comment thread data/migrations/003-update-pipeline-stage-prompts.js
…ation and setup-data drift check

Addresses Copilot review PRRT_kwDOQx8jQ86CRlzw: on Windows checkouts with CRLF
line endings, raw MD5 of an unmodified .md file diverges from the hardcoded LF-
based hashes, causing the migration to incorrectly treat unmodified installs as
customized and skip the length-profile prompt update.

Normalize \r\n → \n and bare \r → \n before hashing in both:
- data/migrations/003-update-pipeline-stage-prompts.js (md5 helper)
- scripts/setup-data.js (drift-detection md5 helper)

All five NEW_SHIPPED_MD5 constants remain correct — sample files are checked in
with LF so normalized and raw hashes are identical in the LF case.

Also applies the previously-deferred ComicScriptStage simplification: remove the
onMouseDown/ref blur-race guard in favour of a simpler persistCoverScript that
never touches imageJobId/prompt, eliminating the race without needing special-
cased ref tracking.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 30 out of 30 changed files in this pull request and generated 3 comments.

Comment thread client/src/components/pipeline/stages/ComicScriptStage.jsx Outdated
Comment thread client/src/components/pipeline/stages/ComicScriptStage.jsx
Comment thread scripts/setup-data.js Outdated
…t, actionable drift warnings

server/services/pipeline/issues.js:
- Add a per-issue Promise-chain serialization queue (queueIssueWrite) and
  wrap both updateIssue and updateStage in it. Each PATCH against a given
  issue id awaits the previous PATCH for that issue before reading state,
  so a blur "cover.script" save can no longer race the render-cover route
  by reading a snapshot from before the render route wrote its imageJobId.
  Each pipeline gets the freshest persisted record at write time.

ComicScriptStage.jsx:
- Add a 5-second "unknown-status expired" timer keyed on cover.imageJobId.
  When MediaJobThumb cannot fetch a persisted job (e.g. archive expired),
  status stays 'unknown' indefinitely; previously this kept the Render
  cover button disabled forever. The button now re-enables 5s after the
  status fails to leave 'unknown', so the user can queue a replacement.

scripts/setup-data.js:
- Classify drift into auto-updatable (matches OLD_SHIPPED_MD5 — migration
  003 will fix it) vs customized (matches neither old nor new — needs a
  manual merge). Emit different warnings for each: auto-updatable points
  at `npm run migrations`; customized points at the data.sample copy and
  asks the user to merge. Previously the warning told users to run
  migrations on customized files where migration would silently skip
  them, leaving the warning permanently active.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 30 out of 30 changed files in this pull request and generated 2 comments.

Comment thread server/services/pipeline/issues.js Outdated
Comment thread server/services/pipeline/issues.js Outdated
Round 11 review caught two issues with the round-10 per-issue write queue:

1. The Map<issueId, Promise> only serialized writes for the SAME issue id;
   two writes to different issues against the shared pipeline-issues.json
   could still read the same initial state and the later atomicWrite would
   overwrite the earlier change. Fixed by collapsing to a single file-level
   Promise chain (issueWriteTail) — every updateIssue / updateStage call
   awaits the previous tail, regardless of issue id.

2. The Map entries were never removed, growing unbounded as new issues were
   updated over the life of the process and retaining resolved payloads on
   the chain. Fixed by tracking only the current tail and releasing it back
   to Promise.resolve() when this write settles AND no later write has
   replaced it as the tail. No GC pressure regardless of issue churn.

The "simple re-entrancy guard" idea from CLAUDE.md still applies — this is
just a coarser guard at the file boundary, where the original race actually
lived.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 30 out of 30 changed files in this pull request and generated 4 comments.

Comments suppressed due to low confidence (1)

client/src/pages/PipelineIssue.jsx:168

  • Generation settings are persisted asynchronously, but the stage continues to read render/refine options from issue.stages[stageId].genConfig until the PATCH response updates parent state. If the user changes the backend/LLM override and immediately renders or refines, the request can still be sent with the previous config. Consider applying the new config optimistically or disabling the affected actions until the save completes.
  const handleGenConfigChange = async (next) => {
    const updated = await updatePipelineIssue(issueId, {
      stages: { [stageId]: { genConfig: next } },
    }).catch((err) => {
      toast.error(err.message || 'Save failed');
      return null;
    });
    if (updated) setIssue(updated);

Comment thread client/src/pages/PipelineIssue.jsx Outdated
Comment thread data.sample/prompts/stages/pipeline-season-episodes.md
Comment thread client/src/components/pipeline/stages/ComicScriptStage.jsx Outdated
Comment thread server/services/pipeline/visualStages.js Outdated
atomantic added 2 commits May 15, 2026 00:15
…ording

PipelineIssue.jsx:
- Track lengthProfileSaving while the LengthProfilePicker PATCH is
  in flight, and disable the Auto-run/Run-everything buttons (plus the
  picker itself) until the save settles. Without this, the user could
  pick a new profile and immediately fire auto-run before the PATCH
  landed, causing generation to run against the previous profile's
  size targets. Tooltip flips to "Saving length profile…" while gated.

ComicScriptStage.jsx:
- Honest placeholder copy on the cover-concept input: the masthead +
  issue-number tag are "included in the prompt automatically", not
  "composited in automatically" — the latter implied a deterministic
  post-processing step that does not exist.

visualStages.js (+ test):
- Cover prompt now refers to "issue title" instead of "episode title".
  The cover is for a serialized comic-book issue; the mismatched TV
  wording was steering the image model toward TV-episode framing on
  the cover art.
Three new conventions distilled from the 10-round Copilot review on the
length-profile / per-stage genConfig / comic cover feature:

1. **In-flight saves gate dependent actions.** Sibling rule to the existing
   "Run Now actions must gate on saved state" — covers the case where a
   field's PATCH is async and a button triggers server work that reads it
   (e.g. lengthProfile + auto-run).

2. **Async PATCH races require server-side serialization.** Client-side
   guards (refs, onMouseDown, status checks) are unreliable; the only
   reliable fix is a file-level write queue so every PATCH merges against
   the freshest persisted state. Also clarifies that per-record queues
   miss writes to different records against the same JSON file.

3. **Stage-prompt template changes need a migration.** setup-data.js only
   copies missing files, so existing installs keep their old templates;
   {{template.variable}} additions need a migration entry. Normalize line
   endings before hashing to handle Windows CRLF checkouts. Drift warning
   must distinguish auto-updatable from customized files so the
   remediation stays actionable.
Copilot AI review requested due to automatic review settings May 15, 2026 13:46
@atomantic atomantic merged commit ab0adfb into main May 15, 2026
4 checks passed
@atomantic atomantic deleted the feat/pipeline-genconfig-length-cover branch May 15, 2026 13:52
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 31 out of 31 changed files in this pull request and generated 5 comments.

Comment thread client/src/pages/PipelineIssue.jsx
Comment thread client/src/services/apiPipeline.js
Comment thread server/services/pipeline/issues.js
Comment thread server/routes/pipeline.js
Comment thread client/src/pages/PipelineIssue.jsx
atomantic added a commit that referenced this pull request May 15, 2026
… serialization

Follow-up to PR #233's review loop — 5 findings on the merged commit.

Client (gating dependent actions while async PATCHes are in flight):
- PipelineIssue.jsx: add genConfigSaving alongside the existing
  lengthProfileSaving, set it during the genConfig PATCH, and propagate
  the combined `actionsGated = lengthProfileSaving || genConfigSaving`
  down to every per-stage component that dispatches a server action.
- TextStagePanel, IdeaStage, StoryboardsStage, ComicScriptStage,
  ComicPagesStage: accept the actionsGated prop and gate every
  Generate / Refine / Render button on it, with a "Saving settings…"
  tooltip while gated. Without this, picking a new length profile or
  visual setting and immediately firing a stage action dispatched the
  request with the old server-side targets.
- apiPipeline.js: `generatePipelineComicCover` now accepts an options
  bag and threads it into request(), per the CLAUDE.md convention.
  ComicScriptStage's cover-render handler passes `{ silent: true }`
  so its existing catch-and-toast doesn't fire a duplicate toast.

Server (extending the file-level write queue):
- issues.js: queueIssueWrite now also wraps createIssue and deleteIssue
  — they previously read/modified/wrote pipeline-issues.json outside
  the tail, so a queued stage save could still race them against the
  shared file.
- New `updateStageWithLatest(issueId, stageId, computeFn)` helper that
  runs computeFn(currentStage) inside the queue and applies its return
  value as the patch. updateStage is now a thin wrapper over it.
- pipeline.js extract-pages route: cover preservation moves into a
  computeFn passed to updateStageWithLatest, so it reads the freshest
  persisted cover.script — not the stale snapshot read before entering
  the queue. Fixes the race where a parallel cover-render PATCH could
  land between the read and write, and have its imageJobId clobbered.

Known sibling races to address in a follow-up (same shape, also in
server/routes/pipeline.js): the comicPages/pages/:pageIndex PATCH and
the comicPages/pages/:pageIndex/render route both read the issue
snapshot before splicing/stamping, so concurrent writes against
different page indices can lose each other. Both fixable by switching
to updateStageWithLatest.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants