# Release v2.2.0

Released: 2026-05-17

## Changed

- **Promote a variation into canon with one click (Universe Builder Phase D).** Every variation card under Cast / Places / Objects now has a "Promote to canon" button — the server LLM-expands the `{label, prompt}` into a fully fleshed canon entry (per-kind: `physicalDescription` / `personality` for characters; `slugline` / `palette` / `era` / `recurringDetails` for places; `description` / `significance` for objects), removes the source variation in the same atomic write, and adds the new entry to the universe's canon array. The button gates on whether the universe is persisted (the action reads the saved record) and on the bucket's `kind`: buckets from the **Other** tab open a trunk picker (Character / Place / Object) since their kind is ambiguous. Duplicate names by canon entry are refused with a 409 so a re-promote can't silently fork an existing record. Endpoint: `POST /api/universe-builder/:id/promote-variation`.
- **Universe Builder redesign — tabbed trunks replace the long-scroll layout.** The Universe Builder page is now organized into seven tabs — **Bible / Cast / Places / Objects / Composites / Render** (plus **Other** when un-classified buckets exist) — driven by `?tab=…&bucket=…` URL params (deep-linkable, back/forward works, mobile collapses to a `<select>`). Each canon trunk surfaces the universe's canon entries alongside that trunk's sub-buckets in one card grid: filter by sub-bucket via chip strip, "Bulk-render all Cast/Places/Objects" hits canon + every variation under the trunk in one render run, "Bulk-render this bucket" scopes to a single sub-bucket. The **Render** tab consolidates the batch-render controls + adds per-trunk, per-bucket, "All canon" and "All composites" one-click targets. Server-side: `compilePrompts` now accepts `canonSelection` and a new `promptMode: 'canon'` (`'all'` includes canon too), synthesizing the render prompt from `name + physicalDescription / palette / description / recurringDetails / significance` for entries without a hand-authored prompt. The legacy long-scroll layout is gone.
- **CoS agent completion paths now share `finalizeAgent` + `releaseAgentLane` helpers.** The TUI shell-session path, the direct-CLI path, and the runner-mode completion handler used to each carry their own copy of the lane release / completeAgent / completeAgentRun / updateTask / provider-marker / processAgentCompletion sequence. Drift between them silently broke one path at a time. Lane release + tool-execution tracking now run early (before output I/O) via `releaseAgentLane()`, and the centralized state writes (completeAgent → completeAgentRun → updateTask → provider markers → processAgentCompletion) go through `finalizeAgent()` — a future change in either contract is a one-place fix.
- **CoS runner-agent map no longer leaks on hook crashes.** A throw from any post-completion step (memory extraction, JIRA push, provider marker, etc.) used to skip the final `runnerAgents.delete(agentId)` and strand the entry forever. The completion sequence is now wrapped in try/finally; the error still surfaces but the in-memory state stays coherent.
- **TUI providers are now usable from the manual `/api/runs` runner panel.** The "Run Prompt" button on `/settings/ai-providers` was disabled for TUI providers with a hint that they had to go through CoS tasks. The server-side `executeTuiRun` branch already handled them — the client-side disable was stale.
- **Importer — auto-detect content type, in-place prose edit, and a "Replace all" toggle for re-imports.** The intake form has a new **Auto-detect** button that runs a light-tier LLM pass on the source head and pre-selects the content-type radio (still user-editable, with a confidence + reasoning hint). The review panel now has a collapsible **Prose excerpt** textarea per issue card so you can trim or correct boundaries without re-running Analyze (which burns three heavy-tier calls). Re-importing an existing series surfaces a destructive **Replace all** toggle that wipes existing issues and overwrites arc + seasons — off by default; canon is still merged additively in either mode.
- **Importer — review form no longer drifts from server-known arc roles + shapes.** The client-side hardcoded `IMPORTER_ARC_ROLES_FALLBACK` is gone — `/importer/config` (and the analyze response) now expose `arcShapeIds` alongside `arcRoles`, and the shape dropdown filters to only what the server will accept on commit. Long review sessions (~50 issues with 500K-char prose previews) also feel snappier — issue and season cards are memoized, so unedited cards skip render on every keystroke. The Season fallback label now names the actual lowest-numbered season instead of the misleading "(first season)" — accurate when seasons are sparsely numbered like [S2, S5, S99].
- **Importer — partial-commit retries no longer overwrite parallel-tab arc edits.** When the issue-loop rolls back mid-commit (`IMPORTER_PARTIAL_COMMIT_ISSUES`), the response now carries `context: { universeId, seriesId, arcAlreadyPersisted, skipArcOnRetry }`. The Importer page reads it and drops `arc` + `seasons` + `canonSelections` from the retry payload, so re-clicking Commit only re-creates the failed issues against the freshest server-side state. The screenplay split-logic prompt also no longer conflates the per-type default with a user-requested count — the act-break branch now gates on `isUserRequestedCount`.
- **Universe canon now lives inside Universe Builder.** Characters, places, and objects are managed inline on the universe page — no separate canon page to navigate to. The old canon URL still works as a redirect, and the Series Pipeline link lands you on the same combined view. Pending edits to other universe fields are no longer lost when canon changes are saved.
- **Locking a canon entry now also blocks new reference renders.** Locked characters/places/objects already prevented AI rewrites; they now prevent new reference and clean-plate image renders too, so a locked entry's identity stays frozen across both text and visuals. Disabled buttons explain the lock in their tooltips.
- **Universe Builder categories now carry a canon trunk (`kind`)** so each bucket knows whether it belongs under Cast, Places, Objects, or Other. Built-in defaults are pre-tagged (landscapes/environments/structures → Places, vehicles → Objects); custom buckets default to Other until you sort them. The default `characters` category was retired — canon owns characters now, and any pre-existing character variations get folded into universe canon on upgrade. Foundation for the upcoming tabbed-trunk Universe Builder redesign.
- **Series cover renders auto-file into the universe's media collection, and universe-linked collections are now name-locked.** Volume (season) and issue cover/back-cover renders now drop into the same `Universe: <name>` bucket that Universe Builder fills with concept art — no more orphan covers floating in the gallery. The collection's name is locked while it's linked to a universe: rename attempts are blocked server-side and the UI shows a lock icon instead of the rename pencil. Renaming the universe itself cascades to the linked collection's name automatically.
- **TUI providers now return clean responses via a per-run output file.** Claude Code TUI / Codex TUI runs are now instructed to write their final response to `data/runs/<runId>/tui-response.txt`, which the runner reads back instead of scraping the PTY screen — no more banner art, "5% cost" meters, or "bypass permissions on" hints leaking into stage outputs. Falls back to the existing screen-scrape cleanup when the file isn't written.
- **"From proof" final renders now work with the Codex (gpt-image-2) backend.** Codex CLI's `-i <file>` flag attaches the proof PNG so gpt-image-2 runs in image-edit mode and preserves composition while upscaling, instead of redrawing from scratch. The pipeline UI's "from proof" toggle on covers, back covers, and pages is no longer disabled when Codex is the selected backend; the dispatcher only strips `initImagePath` for the external SD-API backend (which has no i2i wiring). `initImageStrength` is mapped to a natural-language fidelity phrase in the prompt so low strengths keep the proof's layout intact and high strengths leave room for reinterpretation.
- **Generate From Idea now seeds named canon (characters/places/objects) alongside category variations.** The expand LLM call returns rich first-class entities — a protagonist gets a `physicalDescription`, a recurring location gets a `slugline` + `palette` — that merge directly into universe canon. Existing canon entries always win on name collision so a re-expand can't clobber hand-authored records.
- **Arc planning prompts now see universe canon by name.** `pipeline-arc-overview`, `pipeline-arc-verify`, `pipeline-arc-resolve`, and `pipeline-volume-verify` templates gained a "World canon" block so arc planning + continuity findings can ground/flag drift against the actual named cast/places/objects, not just the exploratory category buckets. Unmodified prompts auto-update on launch; customized prompts get a warning + manual-merge instructions naming all four files.

## Fixed

- **Universe Builder writes are now serialized server-side so concurrent PATCHes can't clobber each other.** `createUniverse` / `updateUniverse` / `insertUniverseWithId` / `deleteUniverse` / `recordRun` all share the same `data/universe-builder.json` file. Without serialization, two near-simultaneous saves (e.g. an auto-save plus a "Promote variation" race, or two browser tabs editing different fields) each computed their merged state from the same pre-write snapshot — last write wins, the other change silently disappears. Each mutating call now enqueues onto a single file-level write tail so every write reads the freshest persisted state. Per CLAUDE.md "Async PATCH races on shared records — serialize writes server-side"; the queue is single-tail-per-file, not per-record, because writes to different universe ids still target the same JSON. The pattern (previously inlined in `pipeline/issues.js`, `pipeline/series.js`, and `mediaCollections.js`) is now factored into `server/lib/fileWriteQueue.js` — all four services share one implementation, so a future change to the queue contract is a one-place fix.
- **CoS agents with Open PR enabled now merge their PR + delete the branch after Copilot review goes clean.** Claude Code CLI/TUI agents drive their own `/do:pr` flow, which opens the PR and runs the Copilot review loop but exits without merging — leaving the PR open and the worktree branch leaking. The CoS completion sections (`buildCliCompletionSection`, `buildTuiCompletionSection`, and the inline TUI block in the full/api path) now append an explicit `gh pr merge "<PR_URL>" --squash --delete-branch` step plus a `gh pr view ... -q .state` MERGED-verification step. The merge is skipped when the review loop ended `timeout`/`error`/`guardrail`, mirroring the contract the review-loop follow-up agent already uses.
- **CoS Workflow tab now renders cleanly on desktop and mobile.** The stage columns had only `min-w-[12rem]` with no max — long stage descriptions stretched each column past 300px and forced horizontal scroll even on wide monitors. Columns now use responsive flex sizing on `2xl+` (`2xl:flex-1 2xl:basis-0 2xl:min-w-[11rem] 2xl:max-w-[15rem]`) so descriptions wrap predictably without forcing fixed widths, and the whole pipeline stacks vertically below `2xl` (with arrows hidden) for phones, tablets, and standard-density laptops where 7 stages won't fit horizontally. Cards inside each stage flow into a 2-column grid on `sm`–`2xl` so single-column stages don't waste tablet width. (Follow-up after the initial PR's squash-merge mixed `lg:` children with a `2xl:` parent wrapper — Copilot review flagged the breakpoint mismatch.)
- **Volume cover-concept generation no longer 500s on upgraded installs.** The per-season "generate volume cover concepts" LLM step was shipped with its prompt template but missing from `stage-config.json`, so existing installs hit `Stage pipeline-volume-cover-concepts not found`. Added the config entry and a migration that seeds both the template and the config on next launch — fresh installs were already fine.
- **Pending data migrations now apply automatically on every server start.** Previously only `update.sh` ran `scripts/run-migrations.js`, so a plain pull-and-restart (or `pm2 restart` after manual git pull) left shipped migrations un-applied — new prompt stages, settings renames, and seed data sat in the queue until the user explicitly ran update or `npm run migrations`. The server now invokes the migration runner during boot, before the AI toolkit loads `stage-config.json`, and continues on failure with an error log instead of refusing to start.
- **Pipeline prose stage now honors the per-series provider/model override.** "Draft prose" was dropping the series LLM override and falling back to the default provider — fixed; the override (e.g. Codex on a series-by-series basis) is now respected on the prose stage the same way it already was on idea, comic script, and teleplay.
- **Universe collection provisioning is now atomic and universeId-first.** The Universe Builder render route and the pipeline cover filer both resolve the universe's bucket by `universeId` (not by name) through a new shared helper, so a universe rename, a name-collision with another universe, or two simultaneous first-time filings can no longer fork the bucket or drop a render. Two universes that happen to share a display name keep separate, properly-stamped collections instead of one hijacking the other's.

## Full Changelog

**Full Diff**: https://github.com/atomantic/PortOS/compare/v2.1.0...v2.2.0
