# Release v1.54.0

Released: 2026-05-13

**Full Diff**: https://github.com/atomantic/PortOS/compare/v1.53.0...v1.54.0

## Overview

In-tree `portos-ai-toolkit`, auto-migration of legacy codex provider configs, and a major World Builder upgrade: structured Influences chips, full per-field locking, per-item locks on variations + composite boards, deep routing for selected world, and bible-aware prompt refinement.

## Added

- **World Builder — structured Influences (embrace + avoid).** Each world now carries a top-level `influences: { embrace: string[], avoid: string[] }` field — short canonical reference labels (e.g. "Moebius", "cel-shading", "Ghibli painterly") that the renderer prepends *deterministically* to `stylePrompt` and `negativePrompt` at compile time, with case-insensitive dedupe. Inline chip editor in the Story bible section + chip preview/editor in the Refine modal. Capped at 30 entries × 120 chars per list. The structured field replaces the prior "Style direction:" prose clause inside `starterPrompt` — the user's pinned references now survive every re-expansion deterministically instead of relying on the LLM to remember them.
- **World Builder — per-field locks on the bible + prompts.** Lock toggles on Starter idea, Style prompt, Negative prompt, Logline, Premise, Style notes, and Influences. Locked fields are echoed back unchanged by the refine LLM (server-enforced) AND preserved by the expand merge (client-enforced). Persisted on the world template; toggle saves immediately when the world is already on disk. Locks are AI-only — the human can still edit a locked field directly.
- **World Builder — per-item locks on variations + composite boards.** Each variation row in `CategoryEditor` and each board in `CompositeSheetsEditor` now has a Lock/Unlock icon. On re-expand, the client extracts locked items, sends them to the server as `preservedVariations` / `preservedCompositeSheets` so the LLM is told not to regenerate those labels, then merges locked-first + LLM additions with case-insensitive label dedupe per category. Locked rows render with an accent-colored border.
- **World Builder — deep routing.** The selected world id now lives in the URL: `/media/world-builder/:worldId` and `/world-builder/:worldId`. Back/forward buttons, refresh, and direct deep-links all work. Sidebar selection, post-create, and post-delete navigations go through `useNavigate`.
- **World Builder — bible context in refine.** The Refine modal now sends `logline`, `premise`, `styleNotes`, and `influences` to the LLM alongside the existing prompts; the LLM rewrites all seven fields together so the bible stays narratively coherent with the prompts. Locked fields are excluded from the rewrite client- and server-side.
- **World Builder — current world state passed to expand.** Generate From Idea now sends the full bible/prompt state plus the lock map to the expander; locked fields get a `[LOCKED]` tag in the prompt and the LLM is told to echo them unchanged so its regenerated premise/styleNotes/etc. stay consistent with a pinned logline (etc.) instead of being written in isolation.
- **World Builder — independent locks for embrace + avoid influences.** The single influences lock split into `influencesEmbrace` and `influencesAvoid`. Each list has its own lock toggle in the UI; the embrace and avoid lines in the LLM prompt get `[LOCKED]` flags independently. Legacy `locked.influences: true` is migrated to both per-list locks on read (no data migration step needed).
- **World Builder — drag-to-reorder for influence chips.** Embrace and avoid chips now reorder via `@dnd-kit` (`rectSortingStrategy` since chips wrap). Each chip has a `GripVertical` handle; the renderer prepends them in the new order to `stylePrompt`/`negativePrompt`. 8px activation distance keeps the X remove button and the trailing input click-safe; keyboard sensor enables space-to-grab + arrow-key reordering.
- **World Builder — collapsible Worlds sidebar with deep routing + tight layout.** The Worlds list now collapses to a 32px rail via a localStorage-backed toggle (persists across visits). The page is full-width (no outer page padding) and the sidebar uses `border-r` + tinted bg matching the WritersRoom layout convention. Selected world id lives in the URL (`/world-builder/:worldId` and `/media/world-builder/:worldId`).
- **World Builder — holistic Refine (bible + influences + categories + composites).** The Refine action now sees the *full* world after Expand: feedback can replace specific variations ("swap The Lollipop Bureau for something more Umbrella Academy"), retune composites to reference the new items, propose entirely new categories, and tune the bible — all in one pass. Locked variations and composites round-trip verbatim (server-enforced; the LLM can't override them even by re-using the same label). Locked influence lists become *append-only* in this path: existing tokens stay in order, but the model may add new ones — Expand still uses strict no-touch lock semantics. The modal now renders a diff section per category and for composite sheets, with Added / Removed / Changed / Locked / Unchanged badges so the user can see exactly what's about to land before clicking Apply. Pre-Expand drafts still get today's bible-only refine — the new structure pass kicks in only when `categories` or `compositeSheets` are non-empty.

## Changed

- **`portos-ai-toolkit` is now vendored in-tree at `server/lib/aiToolkit/`** instead of being pulled from npm. PortOS no longer ships an external `portos-ai-toolkit` dependency in `server/package.json` — `createAIToolkit`, `createProviderStatusService`, and the four toolkit Router factories now live next to the rest of the server code. The toolkit's `uuid` dep was dropped (swapped for built-in `crypto.randomUUID()`), so this removes two transitive packages from `node_modules/`. The scaffold-generated app templates (`server/routes/scaffoldPortOS.js`) still emit `portos-ai-toolkit` imports — those are independent apps that can keep consuming the published npm package; PortOS itself does not.
- **World Builder — "Expand starter → variations" button renamed to "Generate From Idea"** so the action's destructive nature is less ambiguous. (The lock UI is the long-term answer to "I don't want this regenerated.")
- **World Builder — refine prompt no longer asks the LLM to maintain a `Style direction:` clause inside `starterPrompt`.** The structured `influences` field is now the canonical "where the world is heading" record. Legacy starter prompts containing `Style direction:` paragraphs are still cleaned up via `collapseStyleDirectionDupes` (defense in depth — keeps the seed from growing unboundedly across iterations on worlds upgraded from the prior schema).

## Fixed

- **Legacy codex provider configs auto-migrate to the `codex-configured-default` sentinel on boot.** PortOS now treats Codex CLI as "no `--model` flag — let `~/.codex/config.toml` decide" via the sentinel, but existing `data/providers.json` files from before that change still pinned real model ids (`gpt-5`, `gpt-5-codex`, `gpt-5.2`) and would pass `--model gpt-5.2` to a Codex CLI that's since renamed those models. The provider loader (`server/lib/aiToolkit/providers.js`) now rewrites any codex entry's `models[]`, `defaultModel`, `lightModel`, `mediumModel`, and `heavyModel` to the sentinel on the first read after upgrade. Idempotent — already-migrated configs are untouched. Triggered explicitly at startup so the migration runs before the first inbound request consults the file.
- **World Builder — expand merge no longer clobbers legitimate empty-string LLM responses.** The `pick` helper in `handleExpand` previously used `(llmValue || draft[key])`, which silently restored a stale `negativePrompt` when the LLM intentionally returned `""`. Now distinguishes `null/undefined` (keep draft) from `""` (apply).
