Skip to content

Releases: atomantic/PortOS

v2.11.0

27 May 07:52

Choose a tag to compare

Release v2.11.0

Released: 2026-05-26

Overview

A pipeline-authoring and voice release. The Editorial Roadmap stops being a
decorative placeholder and becomes a real LLM-driven reader-emotion / plot /
character analysis tool, with a dedicated Reader Map page and per-issue
caching. The Importer grows up: it splits comic scripts on their own
ISSUE / PAGE headers and seeds them verbatim into the comic-script stage
(no rewriting), lets you pick the AI provider & model for extraction, streams
live per-pass progress, and survives a failed issue-split by preserving the
expensive canon + arc work. The voice assistant can now hand coding tasks to
a CLI agent in an isolated worktree, runs on any configured AI provider (not
just LM Studio), reports weather for your configured home location, and keeps
voice timers across restarts without double-firing. Rounding it out are
federation sync-hygiene fixes — a per-category cross-version sync gate, a
merge-union summary in the duplicate-merge dialog, a cascade-unlink tombstone
fix, and bounded sync metadata — plus a cover-forward Pipeline Series workspace
and a shared inline-badge component.

Added

  • The Editorial Roadmap is now a real reader-emotion / plot / character analysis tool. The Plot / Character / Reader chart on a Series page used to be a decorative placeholder — the curves were generated math, "98%" meant nothing, and "Reader: Draft eval pending" did nothing. Now an Interpret reader map button runs an LLM pass over each issue's actual content (prose, falling back to comic script or teleplay) and maps, section by section (scene/paragraph), what a reader feels — emotion, tension, and emotional valence — plus per-issue plot tension and character arcs. The panel's three curves and labeled cards are computed from that analysis: Plot = narrative tension, Character = the detected protagonist and how far their arc advances (with a count of supporting characters who have arcs), Reader = the reader's emotional journey. A new Reader Map page (/pipeline/series/:id/roadmap, reached via "View reader map") shows the full per-issue, section-by-section emotion log and a character-arc breakdown that answers "is this the protagonist? do other characters have arcs?". Analysis results are cached per issue and flagged stale when you edit the content, and you can re-run the whole series or a single issue. Batch progress streams live.
  • Hand coding tasks to an agent by voice. Settings → Voice has a new "Enable coding-agent delegation" toggle (under Tools). With it on, you can say things like "have the agent fix the failing backup test" or "refactor the widget registry and open a PR" — the voice Chief-of-Staff hands the task to a CLI coding agent (Claude Code, Codex, Gemini) that works in an isolated git worktree and opens a pull request for review. Your fast local voice model stays the conversational layer; only the heavy coding work delegates out, so the chat stays snappy. Pick which coding agent runs the task (defaults to your active AI provider → model from Settings → Providers), and the assistant speaks the result when the task finishes (this completion announcement still respects quiet hours, and you can turn it off). The work runs in the background — your working tree is never touched; you review the PR.
  • Set your home location in Settings → General. Enter a latitude and longitude and the voice assistant's weather command ("what's the weather?") reports conditions for where you are instead of a default city. Leave both fields blank to fall back to the default location.

Changed

  • Pick the AI provider & model for the Importer. The Create → Importer intake form now has an "AI Provider & Model" picker (mirroring the Universe and Series builders), so the canon / arc / issue-split extraction can run on a specific provider and model instead of always using each stage's configured default. Leave it on "Default (stage provider)" to keep the prior behavior; the same selection also drives the content-type auto-detect pass.
  • The Importer shows live progress while it analyzes. Analyze runs several AI passes (canon + arc in parallel, then the issue split) over a single request that can take a couple of minutes, so it used to just sit on a spinner. The intake form now shows a live checklist — each pass moves from pending → running → done (✓) in real time — so you can see exactly which extraction is in flight.
  • Comic scripts import verbatim — split, seeded, and rendered without rewriting. A comic script is already issue/page-structured, so the Importer now (1) splits it on its existing ISSUE / PAGE headers — instantly, no LLM, no rephrasing (header-less scripts still fall back to the LLM split); (2) seeds your verbatim script straight into the issue's comic-script stage (not the prose stage), so the pipeline renders pages from your script instead of regenerating its own; and (3) the comic-page parser now understands the common screenplay convention — bare PAGE 1 / PANEL 1 headers with CAPTION / SFX / speaker labels on their own lines — so those pages parse into panels for image generation with your panel descriptions, captions, dialogue, and SFX intact.
  • A failed issue split no longer throws away the canon + arc work. Canon and arc are the expensive extraction passes; if only the final issue-split step fails or times out, the Importer keeps the canon + arc preview and shows a "Retry issue split" button in the Review panel that re-runs only the split — so you don't burn the two heavy passes again.
  • Pick any AI provider for the voice assistant. The voice Chief-of-Staff's language model was hard-wired to LM Studio. Settings → Voice now has an "LLM provider" picker listing every API provider you've configured (LM Studio, Ollama, NVIDIA, or any OpenAI-compatible endpoint) plus a model dropdown with a refresh button that re-queries the provider's available models. Local-only setups are unchanged out of the box — LM Studio stays the default and the LM_STUDIO_URL override still applies — but you can now point voice at a hosted model. The voice service-health badge probes whichever provider you select.
  • Pipeline Series pages are now cover-forward editorial workspaces. The series detail view replaces the long all-volumes scroll with a compact volume selector, cover cards for issues, colorful theme tags, and an editorial roadmap panel for visually debugging plot, character, and future reader-emotion arcs. Missing or in-flight volume and issue covers now show placeholders instead of leaving dense text lists as the primary navigation.
  • Consistent inline badge styling. The peer relationship and connection-scheme labels on the Instances page and the length-target cards in the Writers Room Guide now render through one shared badge component, so their look stays consistent as more badges are added.

Fixed

  • Importer analyze no longer times out on long sources. Each analyze pass feeds the entire source to a heavy-tier model in one shot, which routinely takes longer than the AI runner's 5-minute default — so the issue-split pass would falsely time out and trigger an expensive from-scratch fallback re-run on another provider. The Importer's passes now get a 20-minute timeout, matched to the work they actually do.
  • Merging duplicate universes now shows what's being combined. When you fold two duplicate universes (or series) together, the merge dialog only ever asked you to resolve scalar conflicts like the starter prompt — the list-shaped fields (style prompt and negative prompt chips, categories, characters/places/objects, composite sheets, seasons) were silently unioned with no indication it happened. The dialog now lists each list field the folded copy contributes to, with a running "N total · +N folded in" count, so the no-data-loss combine is visible instead of feeling like those fields were ignored.
  • [peer-sync-per-category-version-gate] Machines on different PortOS versions keep syncing everything except the one changed data type. When one federated machine updates to a version that changes how a single kind of data is stored (universes, comics, or media collections), the others used to refuse the entire sync until they caught up — silently stalling all data between them. Now only transfers of the changed data type wait for the lagging machine to update; every other kind keeps flowing.
  • Voice timers survive a restart and no longer double-fire. A timer or reminder you set by voice ("remind me in 30 minutes to call mom") is now saved to disk, so it still goes off if PortOS restarts before it's due — any that came due while it was down fire as soon as it's back. A single request can no longer arm two timers, so you won't get the same reminder twice.
  • Federated media collections: a merged-away duplicate no longer survives on peers. When a universe (or series) merge tombstones the loser's auto-collection, a receiving peer applies the universe tombstone first — and its cascade (unlinkCollectionsForUniverse / unlinkCollectionsForSeries) was stamping the linked collection's updatedAt to "now", which then defeated the collection's own (older) tombstone under Last-Writer-Wins and left a live duplicate "Universe: X" collection on the peer. The cascade unlink now preserves updatedAt (it's a derived side-effect of the owner's deletion, not a user edit), so the collection tombstone still wins and both machines converge to deleted. Items are never lost — they're always unioned on merge.
  • Federated sync metadata no longer grows without bound. When a duplicate universe or series is merged away (or otherwise force-pruned), its leftover entry in the cross-machine sync base-hash file used to linger forever. Those orphaned entries are now removed as part of the prune, so the file doesn't slowly accumulate dead rows on long-lived federated installs.

...

Read more

v2.10.0

26 May 07:53
3a08594

Choose a tag to compare

Release v2.10.0

Released: 2026-05-25

Overview

A sync-hygiene and authoring release. Federated installs gain duplicate
resolution
and a non-blocking edit-conflict journal so two peers that
independently create or edit the same Universe/Series converge cleanly without
losing data, and Series now belong to exactly one Universe (enforced, with a
migration for legacy orphans). The Writers Room adds a length-and-craft Guide
page
, and the Review Loop reviewer chain is now configurable — including
local LM Studio / Ollama reviewers with no CLI in the loop. Worktree cleanup is
re-enabled behind merge-verified safety gates, the autofixer and calendar sync
now route through your configured AI provider, and the importer ceiling jumps
from 200K to 5M characters. Plus a batch of mobile media-playback and layout
fixes.

Added

  • Writers Room Guide. A new deep-linkable documentation page at /writers-room/guide (reachable via a "Guide" link in the Writers Room header and from ⌘K / voice). It documents the literary length ladder — Microfiction, Flash Fiction, Standard Short Story, Novelette, plus Novella/Novel for context — with both word and character bands, a page-based book-length estimate table (200/300 pages), and craft principles grouped by structure, character, prose, and revision. The length targets and principles live in a single canonical data module (client/src/lib/writingGuide.js, with a classifyByWordCount() helper) so forthcoming editor analyses — including the planned emotional-roadmap evaluator that charts the reader's emotional journey beat by beat — read the same source the docs render from.

  • Sync hygiene: duplicate resolution + a non-blocking edit-conflict journal. Because two PortOS installs (or two machines federated as peers, or two copies of a data/ folder synced through Google Drive/Dropbox) can each independently create a Universe or Series with the same name — and records are keyed by random id, not name — sync used to leave silent duplicates (e.g. two "Clandestiny" universes) and silently discard the loser whenever both sides edited the same record. Two new tabs under Sharing fix this:

    • Duplicates — finds same-named-but-different-id Universes (and Series, scoped within their universe) and offers to Merge (smart field-union: canon, categories, variations, influences, and render history are unioned; genuinely-conflicting fields are resolved field-by-field via a side-by-side diff; child series/issues and media collections are re-pointed to the survivor; the loser is tombstoned and propagated to peers) or Rename one to keep both. The same detection + merge is also surfaced inline on the Universes page — when two universes share a name a warning banner appears at the top of the list with the same Merge / Rename / Keep-both controls, so a duplicate that slipped past sync time can be resolved without leaving the page.
    • Conflicts — when a cross-install last-write-wins merge would overwrite a record both sides edited, the losing version is now archived to a local conflict journal instead of vanishing. Review the diff and Restore mine, Merge specific fields back, or Discard. Convergence is never blocked — peers still agree on the LWW winner; no edit is lost.

    Both surfaces are deep-linkable (/sharing/duplicates, /sharing/conflicts) and reachable from ⌘K / voice.

  • Series now belong to exactly one Universe (enforced). Creating a series requires a universe, a linked series can no longer be un-linked (only moved to another universe), and deleting a universe that still has live series is blocked until they're moved or deleted. A migration adopts any legacy orphan series into a synthesized universe so existing installs satisfy the rule.

  • Configurable Review Loop reviewer chain (with local-LLM reviewer support). The Review Loop's default reviewer chain used to be hardcoded to GitHub Copilot; the new AI Providers → Code Review Defaults panel lets you set an ordered chain (any combination of copilot, claude, gemini, codex, lmstudio, ollama), the stop mode, and whether reviewers may apply fixes. The configured defaults seed ad-hoc tasks (Tasks page) and per-task-type schedules (CoS → Schedule) when nothing's explicitly pinned, and flow through to follow-up Review Loop agents that didn't carry their own reviewer metadata. Two new reviewer kinds — lmstudio and ollama — route the PR diff through a new POST /api/code-review/local endpoint that runs the model selected on the panel against the backend's OpenAI-compatible chat-completions API, so a local LM Studio or Ollama install can act as your code reviewer with no CLI in the loop.

  • Use a different Chrome variant for the PortOS-managed browser. PortOS now reads a chromePath (and macAppBundle on macOS) from data/browser-config.json, so you can point it at Chrome Canary, Chromium, Brave, Edge, or any Chromium-based browser — separating the automation surface from your daily-driver Chrome. Setup (./setup.sh / setup.ps1) and update (./update.sh / ./update.ps1) now offer to install and configure Chrome Canary automatically: on macOS via brew install --cask google-chrome@canary, on Windows via winget install Google.Chrome.Canary. The prompt is interactive-only (CI / non-TTY runs skip silently), idempotent (won't re-prompt once configured), and supports PORTOS_USE_CANARY=1 for headless opt-in. The Browser page's Config panel exposes both fields for after-the-fact edits.

Changed

  • Merge-verified worktree cleanup — reaps merged branches/worktrees without ever risking in-progress work. Periodic worktree cleanup was previously disabled because it could obliterate worktrees still in use by other agents or outside the CoS system. The cleanup now reaps a worktree only when both gates pass: the working tree is clean (auto-generated lockfile churn ignored) and the branch is fully merged into the default branch. Merge detection (isBranchMergedInto) covers both normal merges (ancestor check) and squash/rebase merges (synthesized-tree git cherry patch-id), so it can't be fooled by a squashed history. The daily agent-data-cleanup job now runs this safe reap across both CoS worktrees (data/cos/worktrees/) and the /work / /claim / superpowers trees under .claude/worktrees/ — skipping locked or in-progress trees — then falls back to the existing orphan-integration pass for dead agents whose commits never landed. Because the two gates together mean "nothing pending and nothing to lose," the previous data-loss failure mode is structurally impossible. To make detection bulletproof going forward, the automated review-loop and /do:pr merge commands now prefer a true merge commit (gh pr merge --merge) over --squash, keeping each branch tip in the default branch's history.

  • Autofixer and Google Calendar sync now run through your configured AI provider. Both previously hardcoded claude -p, ignoring whichever provider/model the rest of PortOS uses. They now resolve a configured CLI provider/model and route through a shared, dependency-light invocation path (server/lib/cliProviderRun.js) — the same per-CLI argv conventions (Codex exec -, Gemini stdin, Claude Code -p -) the main runner uses. New UI controls let you pick the provider/model per feature: a Settings › Autofixer tab, and a picker on the Calendar › Config tab (for Google MCP-sync accounts). Both default to Claude Code when unset, so existing installs are unchanged. The picker is restricted to enabled CLI providers — the autofixer must edit files and run pm2, and the calendar sync must call MCP tools, neither of which an API chat provider can do. Migration note: calendar MCP sync no longer honors the legacy CLAUDE_PATH env var — point the provider's command at your binary under AI Providers instead. The Claude-specific --allowedTools MCP flag is now passed only to Claude-family providers.

  • Importer source ceiling raised from 200K → 5M characters. The Create › Importer page previously rejected pasted sources over 200,000 characters — far short of a real novel (a 250-page book runs ~400K chars; long novels reach several million). That cap was an over-conservative "v1" product limit, not a fundamental constraint: the schema layer already allowed 5MB, and modern large-context providers (Claude, Gemini) ingest a whole novel in a single call. The importer now matches the 5MB abuse-guard ceiling, so the only remaining limit is dynamic — the active provider's context window (the entire corpus still goes into one extraction call until chunked extraction lands). Sources that fit the ceiling but overflow the chosen model's window surface a provider error; pick a large-context provider or trim in the meantime. The pipeline's per-issue canon-extract truncation budget was decoupled (stays 200K) since a single issue's stage output is already hard-bounded at 400K and operates at a fundamentally smaller scale than whole-book ingest.

  • Dashboard Quick Image widget — now exposes resolution and negative prompt options (collapsible) alongside the prompt, and renders the result inline: async backends stream the diffusion loading animation (spinner / step counter / latent preview) and resolve to the final image, mirroring the Universe asset slots via the shared MediaJobThumb. Sync (external) backends show the completed image directly. The "Edit" hand-off now carries the chosen size + negative prompt into the full Image Gen page.

  • Universes table — each row now shows a 48×48 thumbnail of the latest image from the universe's auto-managed media collection (the Universe: <name> bucket linked by collection.universeId). Rows without media fall back to a Globe placeholder; a broken file ref also degrades to the placeholder via <img onError>. Applies to both the desktop table and the mobile card layout.

  • **Write...

Read more

v2.9.0

25 May 21:56
ddab459

Choose a tag to compare

Release v2.9.0

Released: 2026-05-25

Added

  • Voice assistant gained new tools: ask for today's calendar, your next event, the current weather, set a timer, and log a workout by voice — plus "what's on this chart?" which screenshots the active tab and describes it with a vision model.
  • Voice assistant now pulls in relevant long-term memories when you ask recall-style questions ("what did I decide about…", "do I prefer…"), so answers draw on what it already knows about you.
  • Video metadata (the video-history.json rows) now syncs between federated machines alongside the video files, so a shared collection's video items render on the receiving machine instead of showing up as missing. Thumbnails are regenerated on arrival.
  • The Universes sidebar entry now expands into per-universe sub-links, matching the Series Pipeline section.

Changed

  • The image lightbox now shows the "Syncing…" placeholder and swaps in the real image the moment it arrives from a peer, instead of showing a broken image during the sync window.
  • The ⌘K command palette no longer shows two ambiguous "Health" rows — the MeatSpace one is now "Body Health".
  • Reverse subscriptions created during peer sync now update the Instances page live, without a manual refresh.

Fixed

  • Peer sync no longer suppresses an entire category's snapshot for a machine that only subscribes to some records — edits to your other universes/series now reach that peer, and a previously-shared record that's marked ephemeral and then deleted now propagates its deletion.
  • A delete that failed to reach a peer can no longer have its tombstone garbage-collected early, which previously let the deleted record resurrect on the next sync.
  • Re-importing a share bucket that resurrects a locally-deleted record now notifies peers, and the importer no longer treats a locally-deleted universe as permanently "missing".
  • Remixing a video clip whose original model has since been uninstalled now tells you it fell back to the default model, instead of silently switching.
  • MeatSpace iCloud writes retry on a couple more transient filesystem errors (EBUSY/EIO) that busy iCloud paths can throw.

Internal

  • The voice UI index now ships the page's visible-text only when the assistant actually needs it (on demand), trimming per-turn payload size.
  • Extracted a shared .env parse/upsert helper for the setup scripts; migrated a duplicated byte-formatter to the shared one.
  • Added a global test setup that prevents test-created records from fanning out to live sync peers, plus assorted hardening to the video render helper and TUI session handler.

Full Changelog

Full Diff: v2.8.0...v2.9.0

v2.8.0

25 May 14:42

Choose a tag to compare

Release v2.8.0

Released: 2026-05-25

Full Changelog

Full Diff: v2.7.0...v2.8.0

Added

  • Local LLMs — choose your engine and manage models in one place. Setup now lets you pick Ollama or LM Studio for on-device models and installs it for you if it's missing. A new Settings → Local LLMs tab shows both engines' status, installs a missing one in a click, can start or stop Ollama, and lets you run both at once. Browse a curated catalog (reasoning, coding, image analysis, embeddings, chat, small & fast, multilingual) or search Hugging Face, install by click or by name, and move a model between the two engines without re-downloading it.
  • Capabilities page — every connected system at a glance. A new page shows whether each integration (AI providers, calendar, brain & memory, voice, Tailscale & HTTPS, genome & health, Telegram, Messages, apps) is set up and healthy, with a direct link to fix anything that isn't. Handy as both a first-run checklist and an ongoing health view.
  • Media collections now sync across your machines. Image collections stay in sync between your federated PortOS machines, including each image's generation prompts — so synced images no longer arrive with their prompt missing. Universes, series, and collections each show a sync badge, and a details panel lets you push to, pull from, or re-sync a specific machine. Collection sync is opt-in per peer.
  • See all your universes in one place. A new Universes list shows every universe with its entity and linked-series counts and when it was last updated, and lets you share, sync, or delete each one inline.
  • Richer local-model cards. Model cards now show how long ago a model was published, its real download size, an approximate RAM requirement, and its capabilities (vision, code, reasoning, tool use, chat, embeddings) as colored icons.
  • 9:16 vertical video preset. The video generator now offers a 576×1024 (9:16) portrait resolution alongside the existing 16:9 landscape — one click for vertical and social clips.
  • Run several code reviewers per task, in order. When creating or scheduling a task you can pick multiple reviewers, set the order they run in, choose whether to stop after the first that finds (or clears) issues, and whether they apply their own fixes.
  • Your assistant can speak up on its own. With proactive voice enabled, your Chief of Staff now talks first for a critical error, a newly ready task, or a high-priority notification — not only when you address it. Quiet hours are still respected.
  • Force-clear deleted records on demand. After a big cleanup you can now purge deleted records immediately from the Instances page instead of waiting for the routine sweep.

Changed

  • Universe pages moved to a list at /universes. The Universe section is now a list you open into individual editors; existing /universe-builder links still work.
  • Machines on mismatched PortOS versions won't corrupt each other. If a peer is running a newer version with an incompatible data format, sync pauses and you'll see a clear note to update. Older peers keep working as before.
  • Fork-aware updates. If you run PortOS from your own fork, the Update tab now offers explicit choices — sync your fork from upstream and update, sync only, or update from your fork as-is — and won't silently do nothing when your fork is behind. Update notifications still track upstream releases.
  • Faster, safer storage for large data. Universes, comic issues, and series now save each record on its own instead of rewriting one big file, so edits are quicker and more resilient. Existing data is upgraded automatically on first launch.
  • Settings tabs everywhere. The Providers and Prompts pages now share the same tabbed Settings header as the other settings pages, so you can move between them without going back to the sidebar.

Fixed

  • Deleting an LM Studio model works again. It previously failed with an "unknown command" error; PortOS now removes the model's files directly, with safeguards so only the intended model is ever deleted.
  • Finished terminal-agent sessions show their final summary again. Agents that completed and quit immediately could leave the results view blank; the summary is now always captured.
  • Long-running manual tasks keep their workspace. The periodic cleanup no longer deletes the working folder of an in-progress hands-on task.
  • Media-collection sync across machines is more reliable. Linked collections no longer duplicate on each machine, you can pull a record from a peer that has a newer copy, and the sync details panel no longer hangs on "Loading…".
  • The Upcoming Tasks dashboard widget no longer blanks on a momentary refresh hiccup — it keeps showing the last loaded tasks.
  • Failed dashboard layout switches snap back to the layout that's actually active instead of getting stuck.
  • Scheduled planning agents no longer collide on the same to-do item when two run close together.
  • Codex agents can now open pull requests (previously blocked by a sandbox restriction).

v2.7.0

23 May 17:01
a71c42d

Choose a tag to compare

Release v2.7.0

Released: 2026-05-23

Full Changelog

Full Diff: v2.6.0...v2.7.0

Added

  • Peer-sync coverage for media collections, closing the gap where per-universe and per-series image/video buckets (the data/media-collections.json records that drive the "Universe: X" galleries with 74 / 124 / etc items) never propagated to peers. The file sat outside every sync category, and the per-record push pipeline couldn't help either: collection item adds emit recordEvents.updated('universe', id), but the universe record itself doesn't move, so the lastPushedHash short-circuit treated every push as unchanged and the receiver's collection diverged permanently. Fix is in three layers — (1) new mediaCollections snapshot category in server/services/dataSync.js with a mergeMediaCollectionsFromSync entrypoint that LWWs scalars and unions items by kind:ref so neither side ever loses a render to a collection-level updatedAt race; the per-collection write goes through the same serializeFileWrite tail every other mediaCollections mutator uses. (2) The universe / series push payload in server/services/sharing/peerSync.js now bundles the linked collection (findCollectionByUniverseId / findCollectionBySeriesId) and the asset manifest includes each item's ref so image bytes flow through the existing receiver-side asset-pull worker — simplePayloadHash covers linkedCollection so the short-circuit can no longer collapse a collection-only edit. Tombstone pushes deliberately skip the collection bundle so a deleted universe doesn't re-create an empty bucket on the peer. (3) applyIncomingPush routes the bundled linkedCollection through mergeMediaCollectionsFromSync so the receiver's JSON converges atomically with the record merge. DEFAULT_SYNC_CATEGORIES in services/instances.js, the route enum in routes/dataSync.js, AND the syncCategoriesSchema Zod in routes/instances.js all gain mediaCollections (Zod's default object parsing strips unknown keys, so PATCH syncCategories.mediaCollections would otherwise silently no-op — same regression class the universe + pipeline schema fix patched earlier). The client's SYNC_CATEGORY_META adds the row.

  • ephemeral: true field on universes, series, and issues — a local-only "don't sync to peers" marker. Set on POST (create) or PATCH; sanitizer normalizes anything non-true back to absent. The wire / push / subscribe / merge / asset layers ALL honor the flag end-to-end:

    • Wire (sanitizeRecordForWire): returns null for live ephemeral records so both transports (60s snapshot loop + per-record push) skip them. Tombstones for ephemeral records still cross the wire — a once-shared record marked ephemeral then deleted still propagates its delete so peers that still hold the live copy converge. The ephemeral field itself is stripped from every wire output so the byte-stable checksum invariant survives even when a stray ephemeral: false lives on disk.
    • Subscription lifecycle: createUniverse / createSeries skip auto-subscribe for ephemeral records. updateUniverse / updateSeries PATCH transitions are wired: false→true tears down every per-record sub for the record (new unsubscribeAllForRecord helper); true→false re-fires autoSubscribeRecordToAllPeers so the now-shareable record reaches every peer with the matching category. autoSubscribePeerToAllRecords (peer:online backfill) filters ephemeral records out of the set-diff.
    • Receiver-side merges (mergeUniversesFromSync / mergeSeriesFromSync / mergeIssuesFromSync): strip inbound ephemeral field unconditionally (a buggy/older/non-conformant peer can't plant a "dark" record on the receiver), and refuse the merge entirely when the LOCAL record is ephemeral — peer edits can't overwrite content, peer deletes can't trigger our orphan cascade, and the post-merge asset-pull worker never schedules downloads for local-ephemeral records.
    • Series asset manifest: ephemeral AND deleted child issues are filtered out of the asset-manifest input to buildAssetManifestForSeries (not just out of the bundled issues array). Their tombstones still ride in the bundled issues so the receiver's delete cascade can finish — only their asset filenames are stripped, so the receiver doesn't pull bytes for records it's about to orphan. Same rule applies at the top level: a tombstone push for a deleted universe or series ships an empty assetManifest (the deleted record itself still crosses so the peer can converge).
    • peerSync listener now also subscribes to recordEvents.deleted (not just 'updated') so soft-deletes propagate via the per-record push pipeline — previously they were stranded for any peer that had a per-record sub (which is most peers under the new auto-subscribe). retryPendingPushesForPeer walks every sub (not just unpushed ones) on peer:online; the lastPushedHash short-circuit inside pushRecordToPeer skips the network call for unchanged records, so out-of-band file edits (cleanup scripts, hand-edits) re-propagate on the next reconnect.

    New scripts/cleanup-test-data.js one-shot soft-deletes records matching a conservative test-fixture name roster (Test Work, Commit U, Commit S, etc. — generic names like 'A'/'B'/'Live'/'Hidden' deliberately excluded to avoid clobbering real user records), cascading the delete to child issues of each tombstoned series so the orphan-zombie state never materializes. Default is dry-run; --apply writes; users restart PM2 afterward to trigger propagation.

  • quick-image dashboard widget — a textarea + Generate button that hands the typed prompt off to the Image Gen page via ?prompt=… (the same query-param the Media History remix flow already consumes). Registered in widgetRegistry.jsx and seeded into the default Everything layout in server/services/dashboardLayouts.js. Existing installs pick it up via migration 033-seed-quick-image-widget.js, which inserts the widget at its preferred grid slot below quick-brain when free and appends below the existing grid otherwise; only the built-in default layout is touched. Cmd+Enter in the textarea submits.

  • Federated peer-sync tombstone GC (Stage 5 of 5). New server/services/sharing/tombstoneGc.js runs once per 60s sync tick and prunes tombstoned universes / series / issues that every subscribed peer has acked + a 24h grace buffer. The cutoff is min(now, minAckedAcrossPeers) - 24h, which collapses the "no peers subscribed" and "peers behind" cases into one formula (Infinity from an empty peer list clamps via Math.min to now). Issue tombstones use the series ack horizon since issues ride series pushes. Three new exports (pruneTombstonedUniverses, pruneTombstonedSeries, pruneTombstonedIssues) on the record services are mechanical-prune helpers — the GC module owns the policy. syncOrchestrator dynamically imports sweepTombstones to keep the GC's universe / pipeline / sharing graph off the orchestrator's boot path. Logs only when prune count > 0.

  • Federated peer-sync UI + receiver-side asset pull (Stage 4 of 5). New SyncToPeerButton (client/src/components/sharing/SyncToPeerButton.jsx) appears next to the existing ShareToButton on Universe and Series pages — a dropdown of every enabled peer with a check/circle toggle to subscribe/unsubscribe. New MediaImage component (client/src/components/MediaImage.jsx) wraps <img> and renders a "Syncing" placeholder when the asset 404s (peer-pushed records reference UUIDs that haven't been pulled yet); a peerSync:asset-arrived socket event swaps the placeholder for the live bytes the moment the receiver's worker lands them. The Instances page now shows a per-peer "Live-pushed records" section listing every active sync subscription with a one-click unsubscribe and a ↩ reverse badge for subs that were auto-created by an inbound push. New receiver-side pullMissingAssetsFromPeer worker in server/services/sharing/peerSync.js background-fetches each missing asset from the sender's /data/{kind-dir}/<filename> static mount and writes it locally (capped at 100MB per asset, filenames re-validated against path traversal even after the diff already sanitized them). peerSyncEvents is wired through sharing/index.js to socket clients so the UI updates in real time.

  • Federated peer-sync HTTP routes and orchestrator wiring (Stage 3 of 5). New server/routes/peerSync.js mounts at /api/peer-sync with POST /push (receiver-side; wraps applyIncomingPush with Zod-validated payload + asset-manifest size cap of 2000), GET/POST /subscriptions (list + create local outgoing subs), and DELETE /subscriptions/:id. installPeerSyncListener() now runs alongside installSubscriptionListener in initSharing so one recordEvents 'updated' fan-out drives both the share-bucket and peer-sync transports. syncOrchestrator now consults listPeerSubscriptions(peerId) per cycle and skips the snapshot category for any (peer, kind) pair where a per-record peer subscription exists (universe-sub → skip 'universe' snapshot; series-sub → skip 'pipeline' snapshot) so the push pipeline owns the wire for subscribed pairs and the 60s loop stops re-applying the same records. Asset static mounts under /data/{images,image-refs,videos,video-thumbnails} now pass acceptRanges: true explicitly so the receiver's background asset-pull can resume partial downloads over flaky Tailnet links (default of serve-static, now encoded as a contract so it can't accidentally regress).

  • Federated peer-sync pipeline modules (Stage 2 of 5). New server/services/sharing/peerSync.js holds the per-record subscription store (data/sharing/peer_subscriptions.json), the asset-manifest builder, the push pipeline that serializes a record + its child issues + the SHA-manifest to a peer's /api/peer-sync/push endpoint, and the receiver-side applyIncomingPush tha...

Read more

v2.6.0

22 May 05:35

Choose a tag to compare

Release v2.6.0

Released: 2026-05-21

Added

  • Character reference sheets can now render in multiple styles. Alongside the existing illustrated turnaround, characters can now also render a "Blueprint concept sheet" — annotated front/back/side/3-quarter views with close-ups of facial features, costume details, and accessories, on a clean white background with glowing accent linework over a structured base color (both colors auto-picked from the character's palette). The panel under each character in Universe Builder now shows one render row per registered variant, with its own Generate / Regenerate / Delete buttons and thumbnail. Adding a new style ("noir", "isometric", etc.) is a one-entry change in the server-side SHEET_VARIANTS registry plus a prompt-builder function — the UI catalog endpoint (GET /api/universe-builder/reference-sheet-variants) surfaces every registered variant automatically, so no new routes, schema fields, or storage shape changes are needed per variant. Storage stays back-compat: the legacy referenceSheetImageRef field continues to hold the illustrated turnaround, every other variant lands in a new referenceSheets[<id>] map. Shared readSheetPointer / listSheetPointers / applySheetPointerToCharacter helpers (mirrored client + server in lib/sheetPointers.js / storyBible.js) encapsulate the dual-shape read/write so every prune, purge, exporter, asset collector, and UI callback routes through one place.
  • [codex5-client-error-reporter] Browser-side errors and unhandled promise rejections surface in Review Hub. Crashes that used to vanish into the browser console now land as Review Hub alerts so they can be triaged alongside server errors and CoS work. Repeats of the same error within a day aggregate into a single entry, and bursty render storms are throttled so the hub stays readable.
  • [codex5-dashboard-intent-layouts] Dashboard layouts for Deep Work, Health, and Agent Watch — plus a time-window "morning default." Three new built-in layouts ship alongside Everything / Focus / Morning Review / Ops, picked for task-focus, health framing, and CoS monitoring respectively. Any layout can now carry an auto-activate window (e.g. 06:00–11:00) — when the dashboard opens during that window it loads with the matching layout selected, unless you manually picked something else today. The layout editor adds a one-click "Set as morning default" toggle and a custom time-window picker. The dashboard also surfaces inline "Add <Widget>?" suggestion chips when a layout omits a widget whose data is populated (e.g. Quick Stats shows up once you have apps registered).
  • [versioning-diff-view-per-stage-persist-last-n] Pipeline text stages now keep a version history. Each regenerate snapshots the prior draft (up to five back) and a new "History" button on the Idea / Prose / Comic Script / Teleplay editors opens a side-by-side word diff against the current version with one-click restore.

Changed

  • [per-stage-timeout-overrides] Each prompt stage can now carry its own timeout, and provider/stage timeout inputs show a human duration. stage.timeout (ms) overrides the provider default whenever a stage is run via runStagedLLM; callers can also pass timeoutOverride to beat both. The Prompts manager (stage editor) and the Writers Room's inline stage picker both gain a "Timeout override (ms)" field with the active provider's value as the placeholder, and the AI Providers timeout input now displays a ≈ 15m 0s per run hint below the box. min/max/step are wired (1000 / 1,800,000 / 1000) on every timeout input so the OS-level spinner is sensible. Server-side: PUT /api/prompts/:stage validates the timeout field via Zod (rejects garbage like "abc", coerces numeric strings, caps at 30 min); the runner also coerces a legacy stringified stage.timeout so older installs still work; non-positive values are ignored so a stray 0 can't silently cancel runs.
  • Writers Room analysis runs now show live elapsed-time feedback, and the Claude Code CLI provider timeout cap is raised to 30 minutes (default 15). Long Opus-on-prose runs (e.g. writers-room-script with a 35k-char prompt) were dying at the old 5-minute provider cap; the in-tree provider for Claude Code now defaults to 900s and the validation cap on every provider's timeout field is raised to 1800s. The Writers Room work editor also gains a persistent status banner under the header while an analysis is running — analysis label, mm:ss elapsed, and reassurance text that escalates from "Working…" to "Still going — Opus on long prose runs 5–10+ minutes" so the UI no longer goes silent during multi-minute waits.
  • plan-task scheduler now skips dispatch when PLAN.md has nothing to do, and the plan-task prompt waits for code review before merging. The scheduled plan-task agent used to spin up an LLM round even when PLAN.md was empty, only had <!-- NEEDS_INPUT --> items, or had > ⚠️ DRIFT: flags on every unchecked entry — the agent would just exit cleanly, burning a model call for no work. The scheduler now classifies the plan locally (cheap, no git/gh round-trip) and short-circuits the dispatch entirely in those cases, plus when every otherwise-eligible item is already claimed by another agent in flight. feature-ideas is intentionally never gated — its job is to brainstorm into an empty plan. The plan-task prompt itself also gained an explicit "wait for the configured reviewer's findings" step in Phase 6 between gh pr create and gh pr merge; previously gh pr merge --auto waited only on required status checks and could merge before Copilot's auto-review even arrived (as happened to PR #418). The reviewer is now configurable per scheduled task — pick Copilot, Claude, Codex, or Gemini from the Schedule tab, same picker as the add-task form. Copilot polls for the auto-review and addresses comments before merging (cap 3 iterations); the local-CLI options hand off to /do:rpr --review-with <agent>. The prompt's opening line and Phase 1 header were also rewritten so the dual scheduler-pre-reserved / agent-picks paths read clearly instead of contradicting each other.
  • GOALS.md now reflects the full app. Added two missing core goals — Creative Production Suite (Writers Room, Universe Builder, Series Pipeline, Creative Director, Image/Video Gen, Timeline, LoRAs, Importer, Sharing) and Cognitive Training & Lifelong Learning (POST, Morse, Rapid Reader, Wiki). Reframed Purpose and tagline around "everything app," "create more than consume," and "interface with the world and with myself." Updated Status table and Secondary Goals (music/audio production, mood board, sketch canvas, federated sharing, cross-domain insights).
  • [cover-prose-input-idea-input-in-canonusage-corpus] Universe canon cross-reference test suite now pins every stage the per-issue search reads — a regression that silently drops a stage from the search now fails the test suite immediately.
  • [extract-shared-requiretoolkit-helper] Internal: consolidated three duplicated AI toolkit accessors into one shared module. No behavior change; cuts ~30 lines and makes future toolkit-state changes one-touch.
  • [backport-pre-023-migrations-to-_lib] Internal: collapsed seven settled prompt-update migrations onto the shared _lib.js helper. Drops ~465 lines of duplicated migration scaffolding so future prompt-replace migrations are easier to read and review. The shared helper now also handles the rename/retire case migration 003 needed for the long-renamed pipeline-tv-script.md template — older installs that still carry that file get it removed cleanly on next startup.
  • [bump-stale-shipped-md5-on-settled-migrations] Internal: prompt-replace migrations 003 / 004 / 005 / 013 now track the live data.sample hash and ship per-migration test files. Each migration's NEW_SHIPPED_MD5 was advancing-and-then-stale (e.g. 005's pipeline-arc-overview.md had been re-evolved by 019, 013's pipeline-comic-script.md by 027), so a fresh install whose data/migrations.applied.json was reset would misclassify the live sample as "customized" and skip the upgrade. Bumped each NEW hash to the current live sample, appended every intermediate hash to ACCEPTED_OLD_MD5 per the 019 convention, and added a <num>.test.js for each migration so the drift catch fails fast on future template edits. The shared runPromptMigrationTests helper grew a createIfMissing opt-in so migration 005's missing-file branch can be asserted directly.
  • [codex5-bundle-lazy-routes] Dashboard widgets load on demand. A dashboard layout now only downloads the widgets it actually uses — the first paint of a slimmed-down layout no longer drags in every widget in the registry, and slow widgets render their own placeholder while loading instead of blocking sibling cells. Also adds an npm run build:analyze script for inspecting bundle composition during development.
  • [strip-stage-runhistory-from-list-payloads] Pipeline list views stay lean as version history grows. The sidebar's recent-issues list and the per-series issue list no longer ship each stage's full draft history on every read — the History button on an issue's detail page still shows the full set, but list responses can no longer balloon to many megabytes once a series has many issues with deep histories.
  • GOALS.md is now purely strategic; CoS runtime guidance lives in docs/GOALS_OPERATIONAL.md. The per-domain operational goals, task-generation priorities, and core principles that the autonomous agent reads have been split out of GOALS.md so each file has a single concern. No behavior change — the Goal Progress dashboard still reads the same goals; only the file path moved.
  • [useautorefetch-poll-result-dedup] useAutoRefetch callers can now opt into skipping re-renders when the poll result is logically unchanged. The hook accepts an optional compare(prev, next) callback; whe...
Read more

v2.5.0

20 May 23:05
a0f1f2c

Choose a tag to compare

Release v2.5.0

Released: 2026-05-20

Added

  • Network Exposure dashboard widget. Shows how PortOS is exposed on the network — HTTP vs HTTPS, bind address (loopback / all-interfaces / specific), cert mode (Tailscale-trusted hostname vs self-signed), the loopback HTTP mirror port when HTTPS is on, and whether the current browser origin allows mic/voice. A red banner appears when PortOS is bound on all interfaces over plain HTTP. Seeded into the default "Everything" and "Ops" dashboard layouts.

  • CoS task form: pick a reviewer for the Review Loop. A compact dropdown next to the Review Loop checkbox lets you pick the post-PR reviewer (Copilot / Claude / Gemini / Codex; default Copilot). Non-Copilot choices skip the Copilot pre-request and tell the follow-up agent to drive an iterative CLI-based review against the PR diff instead. Agent cards show the selected reviewer when it isn't the default.

  • Deep-linkable image preview modals. Every page hosting an image gallery (Media History, Media Collections, Image Gen, Video Gen, Universe Builder variations + cast, Comic Script Stage, Nouns Stage) now drives its lightbox via a ?preview=<filename> URL parameter — so a previewed image is shareable and survives reload. The browser back button closes the modal; prev/next navigation keeps history clean.

  • Auto-clean after image generation — per-provider opt-in. New checkboxes in Settings → Image Gen turn auto-cleaning on independently for the Codex, Local, and External providers. When on, every generated image is denoised and stripped of C2PA provenance metadata before it shows up in the gallery — what you see is already cleaned, no extra click. Clean failures fall back to the original image and never break the generation.

  • Per-render "Auto-clean this render" checkbox on the Image Gen form. A checkbox under the Generate button lets you override the saved auto-clean default for a single render; the label flips to "(overrides saved default)" when it differs from the saved value. Quietly fixes a latent bug along the way: the saved auto-clean toggle previously had no effect for Codex or Local renders — only External worked. All three providers now honor the saved setting.

  • Lightbox: original ↔ cleaned variant toggle. When the previewed image has a manually-cleaned sibling (or is itself a cleaned sibling), a "View: Original | Cleaned" segmented control appears at the top of the lightbox details panel so you can flip between variants without closing the modal. The toggle is also a deep-linkable URL change. Auto-cleaned images replace the original in place, so the toggle correctly doesn't appear for them.

  • Manual clean auto-files into the source's collections. Cleaning an image via the lightbox Clean button used to leave the cleaned copy out of every collection the source belonged to. The cleaned image is now automatically added to every collection that contained the source, so you don't have to re-add it everywhere.

  • Per-bucket exporter hash cache. Subscription re-exports of large series are dramatically faster — assets are no longer re-hashed on every export. A 200MB re-export drops from roughly 200MB of disk reads to a single index load plus one stat per file.

Changed

  • Honest SynthID copy in the Image Cleaner. The Clean tooltip, the auto-clean settings descriptions, the per-render Image Gen checkbox, the Image Clean dev page, the success toast, and the palette keywords now all explicitly say the cleaner removes C2PA provenance metadata but does not defeat Google's SynthID watermark. The prior copy ("strips C2PA + reduces AI artifacts") was technically accurate but implicitly over-promised against SynthID-bearing renders (gpt-image / Imagen / Gemini). The Image Clean dev page now links to openai.com/synthid for verification.

  • Image cleaning collapsed to a single button. The light/aggressive split is gone — "light" mode proved unreliable in practice (visible C2PA-adjacent noise remained). The lightbox Clean affordance is one button with a clear tooltip; the Image Clean dev page dropped its level picker. Existing cleaned files on disk round-trip unchanged.

  • First-run no longer silently flips port 5555 from HTTP to self-signed HTTPS. PortOS used to auto-generate a self-signed cert on every start when Tailscale was unavailable, which broke the documented http://localhost:5555 URL and forced a browser click-through. Fresh installs without Tailscale now stay on plain HTTP at the documented URL. The setup banner is cert-aware — when HTTPS is provisioned, it prints the loopback HTTP mirror (http://localhost:5553) and the Tailscale hostname instead of the now-broken localhost URL.

  • Universe Builder cast preview now matches Media History exactly. Clicking a cast/canon image used to open a stripped-down modal showing the character description instead of the prompt, with missing controls (no negative prompt, no notes, no Remix / Send to Video / Clean / Favorite). Canon clicks now open the same full-featured lightbox as the rest of the app, hydrated from the gallery sidecar so prompt, negative prompt, model, seed, and resolution all show up. The character descriptor falls back as a label only for legacy renders with no sidecar.

  • Canon entries inline-edit their description. The main description on each canon card (characters / places / objects) was previously a static paragraph with no edit affordance. Unlocked rows now render a buffered textarea — type, blur, save. Pre-migration characters that used the older description field migrate to the new field on first save. The pipeline series view stays read-only on purpose.

  • Universe Canon — Render All per kind, compact single-kind view, deduped lock indicator. A new "Render all (N)" button in each canon kind's toolbar fires reference renders in parallel for every entry that has a description but no image. When the canon section is already scoped to a single kind (deep-linked via ?kind=places or a sidebar entry), the redundant inner card chrome is dropped so the page no longer shows a nested card-in-card. The redundant "🔒 LOCKED" chip on locked entries was removed because the lock toggle button on the right already conveys that state.

  • [med-sharing-server-services-sharing-version-js] Cross-peer share imports salvage older universe canon instead of silently dropping it. Share-bucket imports from pre-B.4 PortOS installs used to lose any series-side canon (characters, places, objects) silently because the post-B.4 series sanitizer strips those fields. The importer now folds that legacy canon into the linked universe (creating a placeholder universe for orphaned series) so the data survives the migration.

  • [med-ai-dispatch-server-lib-tuipromptrunner-js-198] TUI prompt-runner buffer raised from 1MB to 8MB; overflow is now visible. Realistic full-context LLM replies (~600KB UTF-8 from a 200K-token window plus screen chrome) no longer head-truncate mid-token. When the buffer is exceeded, a one-time warning fires and the run record now carries an outputTruncated flag so the Runs page can flag responses where the start may have been lost.

  • [agent-tui-spawn-truncation-warn-parity] Agent TUI spawner surfaces output-buffer overflow. Long-running CoS agents whose failure tail was previously buried behind banner-redraw chatter (and silently dropped on truncation) now record an overflow flag on the agent record with a one-time warning. Distinguishes "long run with overflow" from "clean short run" in error analysis.

  • [med-ai-dispatch-server-services-digital-twin] TUI providers now work for Digital Twin AI features. Settings → Digital Twin previously hung or returned banner garbage when the active provider was a TUI (claude-code, etc.) — the dispatcher was piping stdin to a process expecting a PTY. Twin import, enrichment, testing, and analysis now flow through the shared runner with proper TUI handshaking.

  • [med-ai-dispatch-server-services-aidetect-js-242] TUI providers now work for Settings → Detect App. The "Unknown provider type" error path is gone; TUI dispatch goes through the shared runner with the analyzed directory as the working directory, and JSON parsing is more forgiving of TUI banner output.

  • [cleanplateprompt-migrate-to-rich-canon-fragments] Clean-plate prompts include era and weather. Composing a clean-plate (background-only) image prompt used to drop the era and weather fields from place canon. The full descriptor now flows through, matching how storyboard and comic-panel renders already handle it.

  • [med-pipeline-server-routes-pipeline-js-1416-1452] Cover render no longer overwrites the cover script when the request omits it. Triggering a cover render previously rewrote the persisted script field on every render, racing against any in-flight blur-save of the script. Now: an absent script field preserves the saved script, an explicit empty string clears it, and present text writes through.

  • [med-pipeline-server-services-pipeline-arcplanner-2] Arc-planner no longer guesses wrong on ambiguous season pairings. When an arc-resolve LLM call left more than one season unmatched on each side, the positional-pairing fallback used to silently invent season-id mappings — child issues landed under the wrong season instead of the visible "ungrouped" bucket. The fallback now only fires for the unambiguous 1-on-1 case; multi-side ambiguity correctly routes orphans to ungrouped and logs the cardinality.

  • AI Toolkit warm-up errors return 503 with a typed code. Requests that hit the runner, prompt service, or provider service during toolkit warm-up used to surface as untyped 500s with string-matched messages. They now return 503 AI_TOOLKIT_NOT_INITIALIZED so clients can distinguish "service still initializing" from genuine errors.

  • Mic-availability hint recognizes the full loopback range. The secure-context predicate that decides whether mic / vo...

Read more

v2.4.0

19 May 23:51

Choose a tag to compare

Release v2.4.0

Released: 2026-05-19

Added

  • Season-lock-aware arc resolve. commitSeasonsWithRemap now restores locked seasons field-for-field over LLM-proposed rewrites (via a new mergeSeasonsWithLocks helper) and re-inserts any locked season the LLM drops from the new shape. Auto-resolve + /arc/generate honor per-season locked: true flags without needing to unlock the whole arc — mirrors the per-field arc-lock pattern.
  • worldEntitiesSummary template variable in text-stage prompts. New renderEntitiesSummary helper in server/lib/universePromptRenderers.js produces a compact one-line-per-kind synopsis of a linked universe's named canon (top-N entries per kind, 80-char descriptor cap). Plumbed into loadWorldContext (arc/verify/resolve contexts) and textStages.buildStageContext (per-issue prose/teleplay/comic-script). Migration 027 updates the three text-stage prompts to reference {{worldEntitiesSummary}} in a new "Universe at a glance" block.
  • Character bible speechPattern field. Splits the historic speechAccent (regional/cultural accent) from the new dedicated speechPattern (cadence, sentence structure, lexical tics, recurring phrases — distinct from voiceId, which is the TTS engine pointer). Field is piped through the sanitizer + MERGE_CONFIG, the universe character-expand contract + prompt, the rendered reference-sheet header, the CharacterDetailEditor.jsx UI, and the prose/teleplay/comic-script character render mustache so dialogue carries the character's prose voice. Migration 027 handles existing installs.
  • Delete affordance for character reference sheets. New trash button on CharacterReferenceSheetPanel (inline [Cancel] [Delete sheet] confirm row) backed by DELETE /api/universe-builder/:id/characters/:entryId/reference-sheet. Server unlinks the PNG from data/image-refs/ and eagerly purges every matching referenceSheetImageRef pointer across all universes via a new purgeReferenceSheetFromAllUniverses(filename) helper. Locked characters' sheets stay delete-protected (UNIVERSE_CANON_LOCKED 409, mirrors the renderer's lock guard).
  • Per-stage editorial locks on Pipeline issues. Each issue.stages.{id} now carries a locked flag; locked stages refuse regeneration (text generate, image / video enqueue, refine, extract-scenes / extract-pages, audio extract / render, episode-video fresh start, cover-concepts commit). UI exposes a lock toggle on every Pipeline Issue tab and renders a per-stage Lock indicator on the TabPills strip, so users can freeze a finalized comic script while still iterating storyboards.
  • Per-field arc locks on series. series.locked.arcFields is an opt-in sub-map ({ logline, summary, themes, protagonistArc, shape }); locked fields are preserved verbatim through commitSeasonsWithRemap so arc regenerate + auto-resolve only rewrite the unlocked fields. Inline lock icons on the Arc Canvas read view toggle each field.
  • Pagination on GET /api/pipeline/series/:id/issues. Accepts optional ?offset=N&limit=N; when present, returns { items, total, offset, limit }. Without the params, the endpoint still returns the legacy raw array. Eliminates the silent 1000-issue cap for long-running series.
  • Zod validation across cos-task / loops / cos-job / cos-learning routes. Replaces manual destructure-and-check with safeParse + failValidation for consistent 400 errors.
  • Pending sheet slot cleanup on character/universe delete. New universeCharacterSheetSlot.js module owns the _latestPendingByCharacter Map (extracted from universeCharacterSheet.js to avoid a require cycle) and exposes clearPendingSheetSlot(universeId, entryId) + clearPendingSheetSlotsForUniverse(universeId). updateUniverse diffs cur.characters vs the merged record inside the write queue and drops slots for any character that disappeared; universe delete drops all slots for that universe. Closes the leak where an in-flight render whose character was deleted mid-flight could still stamp referenceSheetImageRef against a missing record.
  • useRowDraft hook for multi-column row drafts. Row-level analogue of useFieldDraft in client/src/hooks/. Preserves the sibling-draft "ride-along" race protection (drafts map merged into the commit payload so a fast A-blur → B-blur sequence doesn't lose column A when the parent hasn't re-rendered yet). ListRow in CharacterDetailEditor.jsx collapses to a single hook call; covered by 9 vitest cases.
  • Module discovery infrastructure — barrels + catalogs. New index.js barrels and README.md catalogs across server/lib/, client/src/lib/, client/src/hooks/, and client/src/services/ enumerate ~80 reusable helpers so new code can grep the catalog before re-implementing (tryReadFile, atomicWrite, optionalBooleanMap, useLockToggle, formatBytes, copyToClipboard, etc.). New "Module Organization" section in CLAUDE.md codifies where new code lives, the discovery rule ("grep the catalog before writing a helper"), the maintenance rule ("every new public module re-exports from the barrel AND lists in the README"), and the namespace-export pattern for domain-prefixed name collisions. server/lib/index.test.js (and matching client tests) enforce the barrel/README contract so a forgotten entry fails boot.

Changed

  • /claim slash command now hard-requires an isolated worktree with absolute paths, a single-Bash-invocation flow, and a pwd verification checkpoint — eliminates the failure mode where the claim branch was checked out in the main repo and blocked further parallel claims.
  • /claim slash command accepts --review-with=<copilot|codex|gemini|claude> to pick the post-PR reviewer. Default copilot preserves the existing /do:pr Copilot loop; codex/gemini/claude skip the Copilot loop and drive an iterative CLI-based review against gh pr diff instead (CLIs invoked the same way the PortOS AI Toolkit drives them — codex exec -, gemini -p, claude -p -).
  • Removed the CLAUDE.md "Worktrees" section that prohibited TUI worktrees; it conflicted with /claim's explicit worktree requirement.
  • atomicWrite migration. 10 services + 4 aiToolkit modules switched from inline ensureDir + writeFile + JSON.stringify to the atomicWrite helper (toolkit gets a self-contained copy at server/lib/aiToolkit/internal/atomicWrite.js). Prevents partial-write corruption on shared JSON state files.
  • Code-quality dedup batch. Eight /simplify follow-up items shipped together: new tryReadFile helper in fileUtils.js (collapses the readFile(...).catch(() => null) pattern at ~60 sites); new optionalBooleanMap Zod helper (3 sites); new assertProvider helper in promptRunner.js (7 sites); new useLockToggle React hook (4 sites in ArcCanvas + PipelineIssue); three more resolveProviderAndModel migrations (agentContentGenerator, agentPersonalityGenerator, loops); TUI text-accumulator skip in runPromptThroughProvider (avoids hundreds of KB of screen-redraw concat per TUI run); loadProviders JSON.parse rescue (renames <path>.corrupt.<ts> instead of crashing boot); new createProvider field-parity test guarding the schema-vs-explicit-list regression noted in CLAUDE.md.
  • AI Toolkit runner: PortOS-aware deleteRun override. Now stops the live child process before deleting the run on disk (closes a zombie-process leak when an in-flight CLI run was deleted via the UI).
  • Better-audit residue cleanups. Four bundled refactors: (1) Shared formatTimecode / formatDurationSec / formatDateShort in client/src/utils/formatters.js (migrated VideoTimelineEditor, VideoTimeline, ImportTab off local copies). (2) feeds.js now sorts data.items on write (addFeed / refreshFeed / refreshAllFeeds), so getItems drops its per-call sort; ensureItemsSorted normalizes legacy unsorted data once on first read with an isSortedNewestFirst guard so already-sorted state isn't re-saved on boot. (3) Typed ServerError codes replace brittle err.message string-checks: voice/tts.js throws UNKNOWN_VOICE (400); nine "AI Toolkit not initialized" throws in providers.js consolidated into one requireToolkit() helper throwing AI_TOOLKIT_NOT_INITIALIZED (503); visionTest.js's misleading "Provider not found" path swapped for an accurate "AI provider service is still initializing" message. (4) Named magic numbers across cos.js (RECENT_TASK_LIMIT = 50, MAX_PARALLEL = 4, etc.) — no behavior change, just self-documenting constants.

Fixed

  • Shell-injection vector in agentLifecycle.js:322. Replaced execSync(\git merge --ff-only origin/${defaultBranch}`, ...) with array-arg spawn-based git invocation (shell: false`).
  • CVEs in server dependencies. basic-ftp 5.3.0 → 6.0.1 (DoS via malicious FTP server, GHSA-rpmf-866q-6p89); protobufjs pinned to 7.5.9 (code injection / prototype pollution); plus transitive ip-address, picomatch, postcss patched via overrides.
  • agentCliSpawning.js stream handlers no longer crash the process. stdout + stderr async (data) callbacks now wrap their bodies in try/catch (CLAUDE.md PTY/child-process rule). Stream-json output is batched via appendAgentOutputLines instead of per-line state writes.
  • toolStateMachine.executions Map capped at 1000 (previously orphaned on crash because eviction only ran via the success-path 60s timer).
  • Voice LM Studio fetch calls now use AbortSignal.timeout(5000). Prevents the voice pipeline from hanging when LM Studio accepts the TCP connection but stops responding mid-model-swap.
  • exportByKind('series'|'universe', ids) now parallelizes exports via Promise.all (was serial for…of).
  • agentTuiSpawning.js doneSentinelTimer no longer silently swallows readFile errors; setInterval body wrapped in try/catch.
  • server/routes/agents.js validates :pid as a number before reaching killProcess/getProcessInfo (`Numb...
Read more

v2.3.0

19 May 15:01
caed39c

Choose a tag to compare

Release v2.3.0

Released: 2026-05-19

Features

CoS / Scheduled Tasks

  • Plan-item ID system for parallel-safe agent work. Every - [ ] checkbox in a managed app's PLAN.md now carries a stable [<slug>] ID, and CoS scheduled feature-ideas / plan-task jobs encode the slug in their worktree branch name (cos/<task>/<slug>/<agent>). Before spawning, the scheduler reads PLAN.md, parses checkboxes, scans git branch -a + gh pr list for any slug already in flight, and pre-picks the first unclaimed item — concurrent agents now sit on distinct items instead of stomping on the same one. Brainstorm runs (no eligible item) generate a slug at insert time. New helper server/lib/planIds.js (slugify + parser + scanner + picker, 17 tests). taskSchedule.js prompts bump to feature-ideas v8 / plan-task v4 with a new {planConstraint} block; worktreeManager.createWorktree accepts options.planId; agentLifecycle.js plumbs it through both spawn paths. Backed by slashdo v2.15.0 which owns the ID-assignment pass via do:replan Phase 0 and preserves slugs across all do:* commands that edit PLAN.md.
  • slashdo submodule bumped to v2.15.0. Brings drift detection in do:replan (flags pending items whose execution would regress newer features) and a persistent Monitor in do:rpr (one stream emits events for new Copilot reviews + CI bucket transitions, replacing the 5+ background subshells that previously accumulated per multi-round review).
  • /claim slash command. Human-driven counterpart to the scheduled plan-task agent — at .claude/commands/claim.md. Reads PLAN.md, picks the first - [ ] whose [<slug>] isn't in flight (scans git branch -a + gh pr list for slug-as-path-segment, skips drift-flagged + NEEDS_INPUT items), creates a claim/<slug> worktree off origin/main, walks the user through verify → implement → archive (PLAN.md → DONE.md with slug preserved verbatim) → /simplify + /do:pr → merge → cleanup. Coexists with CoS sub-agents' cos/<task>/<slug>/<agent> claims because both place the slug as a /-segment visible to either side's in-flight scan. Optional argument lets you target a specific slug for out-of-order work.

Data Sync

  • Pipeline + universe categories sync over Tailscale. Sync orchestrator now covers the creative pipeline state — series.json, issues.json, and universe-builder.json — alongside the existing goals / character / digitalTwin / meatspace categories. Record-level only (no image/video blobs; those stay on the share-bucket path). LWW by updatedAt; local-only runs[] survives every universe merge. The merge runs through dedicated service-level entry points (mergeUniversesFromSync in universeBuilder.js, mergeSeriesFromSync in pipeline/series.js, mergeIssuesFromSync in pipeline/issues.js) so the read-modify-write window lives INSIDE the existing per-file write queue (queueUniverseWrite / queueSeriesWrite / queueIssueWrite) — a concurrent local edit, bible PATCH, season-cover render, or LLM auto-save can't clobber (or be clobbered by) the sync apply. Each entry point also passes incoming remote records through the service's sanitizer (sanitizeTemplate / sanitizeSeries / sanitizeIssue) so older-schema peers (pre-v4 universes missing the canon kind field, etc.) land already-migrated. applyPipelineRemote returns { applied, count, seriesChanged, issuesChanged } where count is records actually changed/added this cycle (NOT total post-merge records — the prior shape over-reported when callers summed cycle deltas). Client Instances page adds Universe + Pipeline category meta with Sparkles + Film icons.

Universe Builder

  • Lock-by-default for canon, variations, and composite sheets + per-row render thumbnails. Universe canon entries (characters / places / objects), category-bucket variations, and composite sheets are now locked on creation so AI rewrite paths (refine, differentiate, expand-merge) skip them until the user explicitly unlocks. Lock guards prompt-data rewrites only — render paths stay open on locked entries (rendering an image doesn't mutate the prompt; it just appends to imageRefs[]). Server-side: sanitizeVariation + sanitizeCompositeSheet default locked: true, applyCanonExtras in storyBible.js now persists both locked: true and locked: false (the writers-room legacy "absent = unlocked" shape stays for callers that never set the flag), sanitizeTemplate runs a defaultLockCanon pass on universe canon arrays so existing on-disk records read locked, and extractCanonFromProse defaults autoLock: true (with explicit autoLock: false opt-out that stamps locked: false on new inserts to survive the universe-side default). promoteVariationToCanon stamps locked: true on the promoted entry. Two new bulk endpoints: PATCH /api/universe-builder/:id/canon/:kind/lock-all and PATCH /api/universe-builder/:id/variations/lock-all (the latter accepts { category, includeSheets }). Client adds a single icon-only lock-all toggle to every section header (canon Characters / Places / Objects sections, each category variation bucket, composite-boards section) that mirrors the per-item lock-toggle visual: Lock icon when everything is locked → click unlocks all, Unlock icon for the all-unlocked + mixed cases → click locks all so the toggle always normalizes upward. Per-item lock toggles now write explicit locked: false on unlock so the bit survives the next read. Per-row render thumbnails in every variation and canon card: EntryThumbSlot is a three-state slot (pending / empty / completed) that swaps in a MediaJobThumb spinner during a render, a portrait placeholder box with a Sparkles render button when no images exist yet, or the rendered avatar once complete. Pending state is driven by a new entryJobs: [{ jobId, entryRef }] field on the /render route response + a pendingByEntryId map on the universe-builder page; completion fires through useMediaJobProgress and optimistically appends the rendered filename to the row's imageRefs[]. New xs size on MediaJobThumb (48x80, 3:5 portrait) matches the 2:3 aspect of typical 1024x1536 universe renders so all three states share a footprint and the row doesn't jump mid-render. Clicking any variation thumb opens the page-level MediaPreview lightbox (same modal the gallery uses), built from a previewItems array spanning every variation + composite-sheet imageRefs[] on the draft; canon entries continue to route through UniverseCanonSection's own MediaPreview (which carries the character reference-sheet image-refs/ prefix). Canon cards switched from labeled pill buttons to an icon-only horizontal action strip (Render reference + Clean plate + AI: differentiate + Lock toggle) matching the variation cards. Migration 024-lock-canon-and-variations.js materializes the locked-by-default state to disk: stamps locked: true on every existing canon entry / variation / composite sheet (skips entries with explicit locked: false), mints stable ids for the historically id-less variations + sheets (so future renders can attach imageRefs via the entryRef collection hook), and best-effort back-fills imageRefs[] on variations from surviving completed jobs in media-jobs.json via params.universeRun.{universeId, category, label} matching — historical renders whose jobs have aged out of the queue file aren't recoverable.
  • Character bible / reference-sheet /simplify follow-ups. Nine cleanups bundled from the simplify review during the character bible expansion PR. (1) CanonCard's four character-only props collapsed into a single characterExtensions object so non-character call sites skip them entirely. (2) New client/src/lib/bibleLimits.js mirrors server/lib/storyBible.js BIBLE_LIMITS so client-side max: literals can't drift from the server caps. (3) Single-field useFieldDraft hook shared by CharacterDetailEditor#DraftField and a new per-row WardrobeRow sub-component (replaces the indexed ${idx}:${field} drafts map). (4) runPromptRefine split into a lower-level runPromptRefineRaw so multi-field merges (universeCharacterExpand) can reuse the empty-guard + rationale-trim + log-tag scaffold. (5) resolveGalleryImage / resolveImageRef / resolveTemplateAsset collapsed onto a makePathResolver(getRoot, { extensions, cache }) factory — thunk-only API (throws on non-function getRoot) so test-time mutations of PATHS.x at mock-eval time still steer the resolver; rootAbsPrefix is cached lazily on first call. (6) New shortId(id, n=8) helper in fileUtils.js replaces inline String(x).slice(0, 8) across the universe character family. (7) New createImageGenWaiter extracts the shared imageGenEvents completion-waiter pattern used by sdapi.js#createCompletionWaiter and voice/tools.js#image_gen. (8) flattenStats/flattenPalette/flattenWardrobes/flattenProps/flattenNamedList moved from universeCharacterSheet.js to canonPrompt.js alongside RICH_SPEC. (9) CharacterReferenceSheetPanel#waitForImageRef accepts an AbortSignal so unmount aborts the 3 s HEAD-poll loop instead of letting it issue requests against a dead component. Three sub-bullets deferred to dedicated PLAN entries: [purge-reference-sheet-image-ref-on-sheet-delete] and [clear-pending-sheet-slot-on-entry-delete] gate on character/universe-delete routes that don't exist yet; [characterdetaileditor-listrow-row-draft-hook] carves off the ListRow half of item 3 — the multi-column ride-along race protection can't survive a per-field split, so a useRowDraft hook for multi-column row state is the right successor.
  • Character bible expansion + artist reference sheet. Universe canon characters now carry novelist + graphic-novelist depth and can render a single dense artist reference sheet styled to match the universe. sanitizeCharacter in server/lib/storyBible.js gains 15 prose field...
Read more

v2.2.0

17 May 21:18
902e297

Choose a tag to compare

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-...
Read more