# Release v2.1.0

Released: 2026-05-16

## Added

- **Create Suite — Importer page (v1).** New on-ramp to the Create pipeline at `/importer` (also reachable via `⌘K → Importer`). Paste a finished short story / novel / screenplay / comic script + Universe Name + Series Name + content-type radio; the server runs three `heavy`-tier LLM passes (canon + arc in parallel, issue-proposal chained behind the arc summary), returns a preview, and the Review phase lets the user toggle per-entry canon checkboxes + edit the arc + issue titles / synopses / arc roles / per-issue season pickers before committing. On commit, `mergeExtractedBible` LWW-merges characters / places / objects into the universe (case-insensitive name match — preserves locked entries, never duplicates), `series.arc` + `series.seasons[]` are written, and one issue is created per proposal with `stages.prose.output` seeded with the verbatim source excerpt and `stages.idea.input` seeded with the LLM's logline + synopsis. Find-or-create on both universe + series names — re-running with the same names extends the existing world instead of forking a fresh one; series-name match is scoped to its universe so a typo can't cross-link. The series-arc lock is honored end-to-end (analyze fails fast with `ERR_LOCKED` so heavy-tier tokens aren't burned on a doomed run; commit double-checks under the read window). 200K-char hard cap on v1 source size — chunked-extraction follow-up captured in PLAN.md. Three new stage prompts (`importer-canon-extract.md`, `importer-arc-extract.md`, `importer-issue-proposal.md`) ship with per-content-type Liquid `{% if %}` branching; migration `015-importer-stage-prompts.js` seeds them into existing installs (idempotent — never overwrites a customized prompt). Wire-shape `places` maps to storage-shape `settings` at the orchestrator boundary so the UI + prompts already speak the forward-looking Phase 0b vocabulary while the on-disk schema stays unchanged. 11 unit tests cover find-or-create scoping, oversize rejection (no LLM call), locked-arc gating (no LLM call), canon merge correctness, prose+idea seeding, and second-pass dedup. Registered in `navManifest.js` (`nav.create.importer`), `Layout.jsx` sidebar (between Media Gen and Series Pipeline alphabetically), `App.jsx` lazy route, and `apiImporter.js` client wrapper.
- **Universe Canon — per-series filter dropdown.** The Universe Canon page header now shows a Filter dropdown when the universe has more than one series with canon usage. Selecting a series narrows the page to canon entries that appear in that series' issues (derived from the existing `canonUsage` cross-reference, no new API call). Adds a `?series=<id>` URL param so filtered views are linkable; stale ids in the URL drop themselves silently. Per-kind section counts switch to "filtered / total" while a filter is active, and the empty-state for a kind now distinguishes "no canon of this kind in selected series" from "no canon at all". Bonus: the per-entry "Appears in" list is now sorted by `issueCount` descending (most-used series leads) with an alphabetical tiebreaker so the cross-reference reads consistently when two series tie.
- **Pipeline series — per-series Universe style override.** Series linked to a universe can now set a `stylePromptOverride` that prepends ahead of the universe's `influences.embrace` for every image-gen call from that series. Lets a single series in a shared universe deviate visually (e.g. a noir spin-off) without forking the universe — the universe aesthetic still trails so it stays visible to the diffusion model. Server-side composition lives in `applyWorldStyle` in `visualStages.js`; client previews (NounsStage ref renders, clean plates) mirror the same composition. New textarea on the Series page is visible only when a universe is linked; max 1000 chars; empty falls through to universe-only style.
- **Settings → Image Gen → HuggingFace Token panel.** Adds a first-class place to paste, view, and clear the HuggingFace token used by gated local image models (FLUX.1-dev, FLUX.2-klein). The panel shows live status with source attribution (stored in `settings.json` vs `HF_TOKEN` env var vs `~/.cache/huggingface/token` from `hf auth login`) so it's clear which slot a token lives in; the Clear button only appears for stored tokens (env/CLI come from outside settings). The runner-side `GatedRepoError` message already pointed at "Image Gen settings" — that destination now exists. Backed by `GET /api/image-gen/setup/hf-token-status` (now returns `source`), the existing `POST` for save, and a new `DELETE /api/image-gen/setup/hf-token` for clear. Also: migration `012-mark-flux1-dev-gated.js` retrofits `requiresHfToken: true` + `licenseUrl` onto the FLUX.1-dev entry in existing installs' `data/media-models.json` (fresh installs were already covered via `data.sample`), so the inline banner on the Image Gen page surfaces for that model too — same flow that previously only worked for FLUX.2-klein.
- **Pipeline — issue back covers + volume (trade-paperback) covers + volume PDF.** Every comic issue now generates both a front and a back cover concept as part of the LLM comic-script pass (new `## Back cover concept` section, parsed alongside the existing `## Cover concept`). Both renders use the proof/final workflow and ship in the issue PDF as `cover → pages → back → colophon`. Each volume (season) gets its own front + back cover concepts via a new per-season LLM step (`POST /series/:id/seasons/:seasonId/cover-concepts/generate`) that seeds `season.cover.script` / `season.backCover.script` when blank (never clobbers user edits). The new `GET /series/:id/seasons/:seasonId/volume.pdf` compiles a full trade paperback: `[vol front] → [issue1 front → issue1 pages → issue1 back] → [issue2 …] → [vol back] → colophon`, sorted by `arcPosition ?? number`. Existing seasons get a "Generate cover concepts (LLM)" + "Compile volume PDF" button on each Arc canvas row to backfill. Migration `011-comic-script-back-cover.js` adds the back-cover section to the installed comic-script prompt for unmodified installs.
- **Universe Builder — per-category "Generate N more" variations.** Each category card now has a Generate dropdown (3 / 5 / 10 / custom up to 50) next to the existing Render / + Add buttons. The LLM is given the universe's bible context (logline, premise, style notes, influences) plus the list of existing labels to skip, and returns N additional variations that get appended to the category. Locked items are unaffected — Generate is purely additive. Auto-saves when the universe is already persisted; otherwise hands you a "review then Save" toast. New endpoint: `POST /api/universe-builder/generate-variations`.

## Changed

- **TUI providers now work for every AI action.** Previously, picking a `type: "tui"` provider (Claude Code TUI, Codex TUI, Gemini TUI) for pipeline prose, refines, or any other staged-LLM call crashed with `Unsupported provider type: tui` because `runPromptThroughProvider` only handled `cli`/`api`. New `executeTuiRun` in `server/lib/tuiPromptRunner.js` spawns the TUI binary directly via `node-pty` (no shell wrapper, no shared-session cap), pastes the prompt via bracketed-paste, watches for the response to complete via output-idle (8s — vs the 180s `agentTuiSpawning.js` uses for long-running CoS agents, since one-shot prompts only have to wait for the model to stop talking), strips ANSI on the fly, and returns the cleaned text. Paste-handshake constants + helpers (`inferTuiCommand`, `applyCommandDefaults`, paste-marker watch, buffer caps) extracted to `server/lib/tuiHandshake.js` so the one-shot and long-running paths can't drift. The same change consolidated 11 ad-hoc call sites that each re-implemented `if (provider.type === 'cli') executeCliRun(...) else executeApiRun(...)` (`agentContentGenerator`, `taskEnhancer`, `universeBuilderRefine`, `agentPersonalityGenerator`, `agentPromptBuilder` JIRA-title, `loops`, `challengeSolver`, `pm2Standardizer`, `brain`, `meatspacePostLlm`) — all now route through the central handler and gain TUI support for free. `runPromptThroughProvider` grew `onData` + `timeout` options to absorb the streaming + per-call-timeout use cases. Net: −754 LOC of duplicated dispatch, one place to fix any future provider-type bug. Headless one-shots that need quieter session behavior (`brain`, `meatspacePostLlm`) still get `provider.headlessArgs` appended via a per-call clone so they don't pollute the user's Claude Code session list.

- **DONE.md condensed to feature-level summaries.** Dropped file paths, LOC counts, and test counts from the per-date entries; grouped by month with one-line bullets so an LLM can scan completed capabilities without drowning in implementation specifics. Per-release `.changelog/v*.md` files remain the source of truth for the rich details.
- **Universe-as-Canon — Phase B.4 schema teardown.** Series records no longer carry `characters` / `settings` / `objects` arrays — canon lives exclusively on the linked universe (Phase B.4 of the Universe-as-canon refactor). Sanitizer, Zod `seriesCreateSchema` / `seriesPatchSchema`, and the universe-canon resolver (`services/pipeline/seriesCanon.js`) all drop their series-side fallback paths; the legacy helpers `extractAndMergeIntoSeries`, `refineCharacterDescription` (and the `nounRefine.js` module), and `purgeImageRefFromAllSeries` are removed alongside the routes `POST /pipeline/series/:id/extract-bible` and `POST /pipeline/series/:id/characters/:entryId/refine`. Post-prose bible extraction (`textStages.js`) and storyboard scene extraction (`routes/pipeline.js`) now flow through `extractCanonFromProse` / `getSeriesCanon` against the linked universe, and the image-delete `purgeImageRefFromAll{Series→Universes}` scan was collapsed to the universe side only. Writers Room `promoteWorkToPipeline` mints a fresh universe per work and seeds it with the work's bibles instead of writing them onto the series; `POST /api/universe-builder` now accepts `characters` / `settings` / `objects` on the create-schema so promote + tests + the share-bucket importer can seed canon at creation. Voice-resolution + dialogue extraction in `audio.js` (`resolveVoiceForLine`, `extractDialogueLines`) now take canon-shaped objects (`{ characters }`) so the route can pass universe canon through `getSeriesCanon`. Stale clients sending the old canon fields on series PATCH/POST get a silent strip server-side rather than a 400. Test fixtures across `series.test.js`, `pipeline.test.js`, `promoteToPipeline.test.js`, and `sharing/integration.test.js` were updated to seed canon on a linked universe.
- **Pipeline series arc — inline theme editing.** Theme pills on the Series Arc card are now directly editable: click a pill to rename, hover for the × to remove, click the dashed "+ Add theme" chip to append (up to 20 themes, 100 chars each — matching `ARC_LIMITS` on the server). Writes are optimistic with a single-flight save gate so a blur-then-click sequence can't double-persist against stale state. The redundant comma-separated themes input was removed from the "Edit arc" form; logline / summary / protagonist arc / shape still live there.
- **Universe Builder — unify style/negative prompts with the embrace/avoid chip editor.** The prose `stylePrompt` and `negativePrompt` textareas have been removed; the universe's style + negative prompts now live as draggable token lists in `influences.embrace` / `influences.avoid` and are the single editing surface (relabeled in the UI as "Style prompt (embrace)" and "Negative prompt (avoid)"). The renderer joins each list verbatim into the rendered prompt the same way the prose fields used to compose. Existing universes auto-migrate on first read: prose tokens are split on commas, deduped case-insensitively against any pre-existing chips, capped at 30 per list, and persisted (`schemaVersion` bumped to 3). The LLM expand + refine flows now emit `influences` only — no more separate `stylePrompt`/`negativePrompt` keys. Stale-client PATCH payloads still carrying prose fields are absorbed into the chip lists server-side so a stale browser tab doesn't 400.

- **CoS prompts — trimmed plan-task template and TUI Completion Workflow preamble.** The `plan-task` default prompt (`server/services/taskSchedule.js`) dropped its `## Constraints` section (redundant with the Phase 1 strict-executor framing), all `Repository: {repoPath}` / `from {repoPath}` / `in {repoPath}` references (the agent is already cwd'd into the worktree), and the duplicated `/simplify` + explicit commit steps that the Completion Workflow already drives — bumped `PROMPT_VERSIONS['plan-task']` to v3 so existing installs with the v2 default auto-upgrade. The shared TUI Completion Workflow (`server/services/agentPromptBuilder.js`, both the inline full-context block and the `buildTuiCompletionSection` helper) lost its "You own the entire commit → push → PR sequence — PortOS will NOT push or open a PR on your behalf" preamble and the PR-body advice paragraph, collapsed to one line ("When the task is complete, run these in order:") followed by the four functional steps (`/simplify` → `/do:pr` → `.agent-done` sentinel → `/quit`). All 19 `agentPromptBuilder` tests still pass.

## Fixed

- **Pipeline arc regenerate — preserve child issues when the LLM mints fresh season uuids.** The follow-up to the `arc/resolve` remap shipped earlier today: the "Regenerate arc" button (`POST /api/pipeline/series/:id/arc/generate` with `commit:true`) was writing brand-new season ids on every run without remapping child issues — every regenerate stranded every issue behind a defunct `seasonId`. Hoisted the resolve path's remap+reassign body into a shared `commitSeasonsWithRemap(currentSeries, { arc, seasons })` helper on `arcPlanner`; the `/arc/generate` commit branch now routes through it, so a regenerate now reattaches existing issues to the new seasons by normalized title → unique `number` → positional fallback (exact mirror of the resolve path). Also added `scripts/repair-orphaned-issues.js --series=<id>` as a one-shot data tool for installs already affected by the gap — runs dry-run by default, requires `--apply` to write, refuses non-1:1 orphan/season counts.

- **Pipeline arc resolve — preserve child issues when the LLM rewrites the seasons array.** Auto-resolve (`POST /api/pipeline/series/:id/arc/resolve`) replaces `series.seasons[]` with whatever the LLM returns. The prompt instructs the LLM to "preserve volume `id` for every existing volume you keep", but when it ignored that and minted fresh UUIDs (or returned seasons without ids), every child issue still pointed at the now-defunct seasonId and vanished from the Arc canvas — the UI groups issues by `seasonId` and never iterated the orphan buckets. `resolveVerifyIssues()` now tags each merged season with its provenance, derives the set of dropped old seasons, and builds an old→new remap (normalized title → unique `number` → positional fallback when counts line up). Affected child issues are migrated to the matched new seasonId (or `null` = ungrouped, which is visible) in the same write, mirroring `deleteSeason`'s bulk-reassign idiom: `updateIssue(…, { skipRenumber: true })` per call + one `recomputeIssueNumbersForSeries` after, wrapped in `withReexportSuppressed('series', seriesId, …)` so a 96-issue series doesn't fan out N socket emits + N debounced re-exports. As a backstop, `ArcCanvas.jsx` now buckets any issue whose `seasonId` isn't in the current `seasons[]` under the existing "Unassigned issues" group instead of a key nothing renders — so non-resolve-path orphans (stale data, future bugs) stay visible too.

- **Sharing exporter — fall back to per-image metadata sidecar when the media-job archive has aged out.** The `mediaJobQueue` archive trims completed jobs after 24h, so subscription re-exports of any universe with older images were spamming `⚠️ sharing.exporter: imageJobId <id> not found in live queue or archive` for every aged-out job, and the recipient's bundle lost the per-job metadata record (prompt, model, dimensions) used for full-fidelity re-renders. `exportMediaJobAndAsset` now falls back to reading `data/images/<jobId>.metadata.json` (written at gen time by both the codex and local image-gen paths, lifetime tied to the asset file) and synthesizes a job record from it when the in-memory queue/archive miss. The warning now only fires when both lookups miss — meaning the image file or its sidecar is actually gone — and the message text now includes `or sidecar` to reflect the new lookup path.

- **Universe Builder — coalesce per-image share re-exports during a batch render.** When a subscribed universe was rendering a large batch (e.g. 160 prompts), every completed image fired a per-item `emitRecordUpdated('universe', …)` → debounced re-export, walking every historical `imageJobId` in the linked collection and warning for any that aged out of the 24h in-memory media-job archive. The render route now registers each run's job count with `universeBuilderCollectionHook`, which wraps every per-item `addItem` in `withReexportSuppressed` while the run is active and emits a single `emitRecordUpdated` once the last terminal event (completed / failed / canceled) lands. A 160-image batch now schedules one debounced re-export at the end instead of ~one per completion, and the "imageJobId not found in live queue or archive" warnings collapse to a single sweep. Server restart mid-run drops the in-memory tracker entry — the hook falls back to the legacy per-image emits for the rest of that run.

- **Media annotations now sync across PortOS instances via shared Drive buckets, with per-author attribution.** Previously, a note or star added on one machine was invisible to peers sharing the same Drive bucket — `data/media-annotations.json` lived purely client-local. Storage is now per-author: each instance owns its own `instanceId` sub-entry under each `<kind>:<ref>` key (`{ authors: { [instanceId]: { authorName, starred, note, updatedAt } } }`), so my notes never clobber yours and vice versa. A new manifest kind (`media-annotations`) carries each instance's annotation snapshot into every auto-merge bucket via a stable filename (`annotations-<instanceId>.json`), debounced 2s after the last local edit. The export is per-bucket scoped to assets actually present in that bucket, so a private annotation can't leak into an unrelated share. On import, peer records merge per-author by `updatedAt` LWW and re-broadcast `media:annotation:updated` so open clients refresh without a reload. Stars are also per-author — "my favorites" remains my view, not a tug-of-war. The lightbox now shows my note in the editable textarea and "Notes from others" as labeled read-only blocks below (with the peer's display name + their star). Migration `012-attribute-existing-annotations.js` rewraps existing notes under the local instance's id on first run.

## Pre-release hardening (from multi-agent review of the v2.0.0 → v2.1.0 diff)

- **Pipeline arc — write seasons BEFORE re-pointing issues in `commitSeasonsWithRemap`.** The original ordering reassigned every dropped issue's `seasonId` first and then persisted the new `series.seasons[]`. A crash inside that window would leave every reassigned issue attached to a `seasonId` that didn't yet exist in the persisted series record — exactly the orphan state this helper was added to prevent. Swapped to seasons-first; UI behavior unchanged, crash-window invariant restored.
- **AI Toolkit — TUI runs are now trackable and stoppable.** `executeTuiRun` registers its PTY in the shared `_portosActiveRuns` map (via new `registerActiveRun` / `unregisterActiveRun` helpers in `services/runner.js`) so the patched `stopRun` / `isRunActive` in `index.js` see TUI runs the same way they see CLI runs. Without this, a hung TUI run silently ignored the /runs Stop button and kept spending tokens until the hard timeout. Mirrors the CLI runner's registration pattern — same map, same delete-on-finalize, IPty's `.kill(signal?)` matches ChildProcess's signature so the existing patch works unchanged.
- **AI Toolkit — `POST /api/runs` now dispatches TUI providers instead of silently no-op'ing.** The toolkit's runs router only branched on `cli` / `api`; selecting a TUI provider for an ad-hoc run created the run record and immediately returned `{ status: 'started' }` without ever invoking an executor — the run sat at `success: null` forever with no error. Added a `tui` branch that calls `runnerService.executeTuiRun(...)` (now attached in `index.js` alongside `executeCliRun`), plus a 400 fallthrough so unknown provider types fail loudly instead of silently. The staged-LLM path (`promptRunner.js`) already handled `tui` correctly; this closes the gap for the runs UI.
- **Importer — switched `{{source}}` fencing from triple-backtick to long-tilde with explicit "treat as quoted input" preamble.** User-supplied source can contain ``` (code samples, markdown excerpts, screenplay quotes), which closed the fence early and let everything after parse as instructions to the LLM. A no-malice-required prompt-injection vector in a feature whose entire purpose is to ingest arbitrary user prose. Migration `016-importer-fence-source.js` updates installed prompts when their hash still matches the original shipped version; customized prompts log a warning so the operator can opt in manually.
- **`migrateSeriesCanon` — preserve `source` / `autoLock` / `sourceSeriesId` provenance.** The migration was calling `mergeExtractedBible` with no opts, so migrated entries landed with defaults (`source: 'ai'`, `autoLock: false`). The live post-B.4 series-extract path (`textStages.js`, `routes/pipeline.js`) stamps `SERIES_EXTRACT` + `autoLock` so a later AI refine can't silently rewrite series-derived canon. Without these opts the migrated rows defaulted to "freely overwritable" — semantically the opposite of what the live path promises. Now passes the same opts so migrated and live-extracted entries behave identically downstream.

## Full Changelog

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