Skip to content

Release v2.19.0#1122

Merged
atomantic merged 84 commits into
releasefrom
main
Jun 10, 2026
Merged

Release v2.19.0#1122
atomantic merged 84 commits into
releasefrom
main

Conversation

@atomantic

Copy link
Copy Markdown
Owner

Release v2.19.0

Released: 2026-06-09

Added

  • Song piano-roll (Synthesia) view: the Song reader's layered MIDI player now has a Staff ↔ Piano toggle. Piano mode renders every selected layer as colored notes falling onto a piano keyboard and lights up each key as its note crosses the hit line — the aggregate of all checked voices played "on the piano" at once. Each layer gets a stable color shared by the falling notes, the keyboard glow, and a swatch on its layer checkbox; the fall stays sample-aligned to the audio by reading the player's live position. The keyboard geometry and per-note MIDI mapping live in a pure, unit-tested helper (pianoKeyboard.js); the multi-score player gained position()/duration() accessors.
  • CyberCity town redesign (part 2): exploration mode now has a playable character — a theme-tinted articulated cyber-runner (two-segment limbs, emissive visor/core/soles, jet vents) with idle/walk/run animations, turning toward movement, banking into turns, and a hover pose during flyover. Third person is the new default exploration camera: a damped follow boom that shortens when it would clip a building; V (or city settings) swaps to the classic first-person view. Grounded movement now stops at the bay shoreline while the Data Harbor piers stay walkable. All the math lives in a pure, fully unit-tested rig module (cityPlayerRig.js).
  • CyberCity town redesign (part 1): the city is now a coherent game town built from a master plan (cityPlan.js — single source of every district's parcel, with invariant tests). The north edge is a bay: animated water, a shoreline that clips the ground grid, and a new Data Harbor pier district visualizing the install's storage — one disk-stack silo per PostgreSQL table (height = rows, radius = size, orbiting ring on pgvector tables, migration obelisk) and one container rack per data/ domain (lit slats = disk usage), with clickable holographic detail cards and a distinct "DB OFFLINE" state, fed by a new GET /api/city/introspection endpoint. Town fabric: an octagonal ring road with spokes to every district and a grand avenue to the harbor, an elevated transit loop with orbiting trams, instanced street lamps with light pools, plaza holo-trees, and deterministic per-app rooftop fixtures. Everything follows the existing theme-driven day/night system and quality presets.

Changed

  • Shell page: The claude quick-launch button now starts Claude with a blank system prompt (--system-prompt .), skipping the heavyweight default harness prompt for interactive sessions. A new claude (full) button preserves the previous behavior (Claude's default system prompt) for when it's wanted.
  • 2026-06-09 DevSecOps audit sweep — a full-codebase audit landed as a set of focused PRs. Highlights: crash-safe (atomic) writes for a dozen state files (memory index/embeddings, backup scheduler, calendar/message accounts, activity logs); timeouts on local-LLM and provider model-list requests so a stalled endpoint can't hang the server; guards on background timer callbacks that could previously crash the process on a rejected write; the Templates page is now reachable from ⌘K and voice navigation; faster first loads on Goals, Brain, Chief of Staff, and CoS Memory (their 3D views now load on demand instead of riding in the main bundle); the bulky googleapis package replaced by the scoped Calendar/Gmail/auth packages (~190 MB less on disk); screen-reader labels on icon-only buttons and form fields across settings tabs; agent World-tab history now reloads when switching agents; and ~120 new tests covering previously untested services.
  • [issue-1081] Maintenance: Split the pipeline API's single 2,500-line route file into domain-grouped sub-routers (audio, series, arcs, manuscript, covers, issues, editorial) — no behavior change, every endpoint keeps its exact path.
  • Docs: Split the AI Toolkit and Dashboard Widgets sections out of the root CLAUDE.md into directory-scoped server/lib/aiToolkit/CLAUDE.md and client/src/components/dashboard/CLAUDE.md files (load on demand when working in those subtrees), leaving discovery pointers at root. Trims root context with no loss of guidance.
  • [issue-1082] Maintenance: Decomposed the Chief-of-Staff task-evaluation engine's monolithic spawn loop into one named function per priority tier — no behavior change, making each tier independently testable.
  • [issue-1083] Maintenance: Split the 2,600-line task-prompt module into a pure data leaf (the default-prompt catalog) and a thin getter layer, removing a circular import between the prompt and schedule services — no behavior change.
  • [issue-1084] Maintenance: AI provider/run/prompt API errors now report the same structured shape (with an error code and timestamp) as the rest of PortOS, so error-handling UI behaves consistently across every API.
  • [issue-1085] Maintenance: The AI run executors now take a single named-options argument instead of long positional parameter lists, making their call sites less order-fragile — no behavior change.
  • [issue-1086] Maintenance: Extracted the sidebar's apps/series/universes data-fetch loops into dedicated, independently-tested hooks — no behavior change.
  • [issue-1087] Maintenance: Extracted the duplicated install-progress streaming logic shared by the FLUX.2 and video-runtime installer dialogs into one tested hook — no behavior change.
  • [issue-1088] Maintenance: Added behavioral tests for the Backup settings tab covering settings save, the database restore confirmation gate, and backup-status rendering — no behavior change.
  • [issue-1089] Repo detection path scoping: The "import a repo" detector now documents its single-user trust model and, when you set PORTOS_WORKSPACE_ROOTS, confines detection to those directories — pointing it elsewhere returns an invalid-path result instead of reading the directory. Unset (the default), detection stays unrestricted as before.

Fixed

  • [issue-1090] Git tab dialogs are now screen-reader friendly — the diff and release-confirmation pop-ups announce themselves as dialogs, their close buttons are labeled, and pressing Escape (or clicking outside) dismisses them.
  • [issue-1091] Cleared two dependency security advisories — pinned patched versions of two transitive packages to resolve a pair of moderate denial-of-service advisories flagged by npm audit, with no breaking changes. A couple of remaining advisories stay blocked on upstream projects publishing compatible major releases.
  • Tests no longer write to a developer's real Postgres (and federate fixtures to live peers). memoryBackend.getBackend() selected the file backend under NODE_ENV=test only when Postgres was unreachable — so on any dev machine with a healthy local portos DB the suite wrote hundreds of fixture universes/series/works straight into the real database, fanned them out to live Tailscale peers via autoSubscribeRecordToAllPeers, and let test-cleanup tombstones delete the user's real records. Test mode now short-circuits to the file backend before the health probe, so a running dev DB is never touched (an explicit MEMORY_BACKEND=postgres still opts a suite into the PG path). Adds a regression test asserting test mode bypasses a healthy Postgres, plus a one-off recovery script (scripts/recover-synced-test-fixtures.mjs) that restores tombstoned-real records, purges fixtures, and prunes the peer-subscription ledger.
  • Sync/Share peer dropdowns no longer render behind the next card. The "Subscribe to peer" (cloud) and "Share to bucket" popovers on series/universe cards were absolutely positioned inside each card, so they were painted under the following card's stacking context. Both dropdowns now portal to document.body with fixed positioning anchored to their trigger (re-anchored on scroll/resize), and a peer missing its name shows a sensible fallback instead of the literal "undefined".

Full Changelog

Full Diff: v2.18.0...v2.19.0

atomantic and others added 30 commits June 9, 2026 14:02
…files

Relocate the two directory-local subsystem sections out of root CLAUDE.md
into server/lib/aiToolkit/CLAUDE.md and client/src/components/dashboard/CLAUDE.md
so their detail loads on demand instead of in every session. Root keeps a
discovery pointer for each. No guidance lost.
…y clients, named constants, a11y labels

From the 2026-06-09 audit. history routes drop the hand-rolled
.catch(next) pattern for asyncHandler (404 via ServerError instead of a
silent hang on a falsy result) with error-path tests added. The
Moltworld/Moltbook integration classes become factory functions per the
no-classes convention (legacy names kept as aliases). Magic numbers get
named constants (Moltbook action delay, Outlook full-body refresh limit
+ truncated flag, Apple Health XML poll/timeout, self-restart delay).
Settings tabs get proper label/input pairing and aria attributes, and
swallowed .catch(() => {}) sites get breadcrumb logging.
From the 2026-06-09 audit. Crash-safe atomicWrite replaces raw
writeFile for a dozen state files (memory index/embeddings, backup
scheduler state, calendar/message accounts, activity logs); a shared
sleep(ms) in fileUtils replaces seven per-module delay one-liners;
tryReadFile/safeJSONParse replace hand-rolled read+parse combos.
Client formatter duplicates (formatBytes, formatCooldown,
formatDurationMin/Ms, parseSizeGb/recommendedRamGb) collapse onto
utils/formatters with new unit tests, and the agents World tab fixes a
stale closure so history reloads when switching accounts. New test
files cover memoryStore and the account services.
…SE write guard

From the 2026-06-09 audit. Async setTimeout callbacks in agent
lifecycle/spawning get try/catch so a rejected updateAgent can no
longer kill the process. Provider test/model-list fetches and the
Ollama release lookup get AbortSignal timeouts so a stalled local
endpoint can't hang forever. The aiToolkit drops its one outward import
(antigravity constants) for a documented internal copy, restoring the
vendored package's self-containment. The voice dictation socket handler
gets the mandatory non-request-path try/catch, the logs SSE stream
guards writes after client disconnect, mergeIssuesFromSync persists
only the issues the merge actually changed, and the task-override
fan-out is bounded with mapWithConcurrency. New tests cover the
provider network layer, voice payload validation, the spawn
initialization timeout, and changed-only sync persistence.
From the 2026-06-09 audit. GoalsTreeView, BrainGraph, the five Chief of
Staff avatar variants, and the CoS MemoryGraph now load on demand
(static barrel re-exports removed so the dynamic imports actually
split — Rollup flagged them as ineffective). MemoryTab's icon-only
buttons and search input get aria-labels, the prompt editor's clickable
div becomes a real button, and roadmap section keys are stable.
.env.example replaces two phantom OPENCLAW vars with the nine the
server actually reads and documents 16 previously-missing env vars
(verified against code).
…kages

From the 2026-06-09 audit. The 194 MB googleapis package shipped 323
API surfaces of which PortOS uses two; the official scoped
@googleapis/calendar + @googleapis/gmail + google-auth-library packages
provide identical call signatures at a few MB. Also bumps the stale
exact sax pin to ^1.6.0, aligns server pm2 to 7.0.1 (matching the root
manifest), and switches goalCalendarScheduler's goals.json write to
atomicWrite while in the file.
… upload paths

From the 2026-06-09 audit. executeCommand now builds its child env via
safeChildProcessEnv like every other spawn site (Malloc debug noise
stripped; PATH/HOME/auth tokens intentionally inherited — allowlisted
commands like gh/git/npm need them, and the choice is now documented at
the call site). The uploads endpoints return /api/uploads/<name> URLs
instead of leaking absolute server filesystem paths. Includes the
audit's changelog entry.
…rvices

From the 2026-06-09 audit. The 214-line imageGen /generate handler
delegates to a new services/imageGen/prepareParams.js; the clean /
de-watermark / light-regen variant endpoints share a new
services/imageGen/variants.js (persist-variant tail deduplicated); the
SSE writeHead/send/safeEnd boilerplate in imageGen/videoGen collapses
onto a shared openSseStream helper in sseDownload.js. llmSchema moves
from a sibling-route import into lib/validation.js, episode canon
extraction moves from the pipeline route into arcPlanner, and the
/templates page registers in the nav manifest so it is reachable from
the command palette and voice navigation. imageGen tests pin the avatar
response shape and add a regenerate happy-path contract.
…in sub-routers

server/routes/pipeline.js had grown to ~2,484 lines / 83 handlers. Split it
into server/routes/pipeline/{audio,series,arcs,manuscript,covers,issues,
editorial}.js assembled by index.js, mirroring the cos.js sub-router pattern.
Pure code motion: route paths, schemas, and handler bodies are unchanged
(verified by diffing the extracted method+path sets — identical 83 entries).
Shared error mapping (mapServiceError), the provider-override schema shape,
and countExtractedCanon moved to pipeline/shared.js. Comment pointers in
sharing.js, migrateSeriesCanon.js, tts.js, and promoteToPipeline.js updated
to the new file locations.

Refs #1081
refactor([issue-1081]): split routes/pipeline.js god-router into domain sub-routers
refactor: history asyncHandler, factory integration clients, named constants, settings a11y
refactor: consolidate duplicated helpers onto shared canonical utilities
fix: crash/hang guards — async timer callbacks, fetch timeouts, SSE write guard, bounded fan-out
perf: lazy-load three.js views; a11y fixes; accurate .env.example
deps: scoped @googleapis packages replace the googleapis monolith; sax/pm2 alignment
security: safer executeCommand env handling + relative upload paths
refactor: extract imageGen/pipeline business logic into services; register /templates in nav
…amed helpers

Extract each spawn priority tier from the ~430-line evaluateTasks loop into a
named private function (spawnPriority0OnDemand, spawnPriority1UserTasks,
spawnPriority2AutoApproved, spawnPriority3Missions, spawnPriority36FeatureAgents,
spawnPriority4IdleReview), plus resolveAutonomyBudget and
unblockExpiredOrphanCooldowns. evaluateTasks now orchestrates the tiers in
sequence over a shared spawn context (ctx), keeping the cross-cutting gates
(paused/daemon, global slot cap, autonomy + daily budget) in the orchestrator so
they uniformly cover every tier. No behavior change.

Repoint the source-level invariant guards in cos.test.js at the new helper
scopes (the engine's logic moved out of the evaluateTasks body).
/simplify pass — no helper destructures ctx.userTaskData (unblock pass takes it
directly; tiers use pendingUserTasks).
…ew: claude,codex)

The two budget/idle invariant guards were widened to whole-module GEN_SRC when
the gate logic moved into spawnPriority* helpers, losing the 'is this fenced
helper actually wired into evaluateTasks?' precision. Add an orchestrator
tier-order guard (mirroring the existing dequeueNextTask one) so a fenced helper
can't drift out of the spawn path while the module-scoped gate guards still
match its orphaned fence text.
refactor([issue-1082]): decompose evaluateTasks priority tiers into named helpers
…district layout

cityPlan.js now owns CyberCity's geography: every district helper anchors its
parcel there instead of hardcoding a compass position, and invariant tests
enforce world bounds, plaza clearance, and shoreline placement. The north edge
(-Z, facing the default camera) becomes a bay: CityWater renders the water
plane + surf line, the ground/grid clip at the shoreline, skyline silhouettes
skip the water, and the voice beacon steps aside for a plaza-to-harbor avenue.
Street/transit topology is computed here for the upcoming street-fabric and
Data Harbor commits.
Read-only diagnostics for the upcoming Data Harbor district: public-schema
tables (pg_stat_user_tables row estimates + relation sizes, pgvector column
flags, database size, applied migrations) and per-domain disk usage of data/.
45s TTL cache with stale-while-revalidate so the multi-second data/ walk on
media-heavy installs never blocks the route; a down DB yields db:null rather
than empty arrays per the absent-vs-empty rule.
…ta/ filesystem

One disk-stack silo per Postgres table on the western pier (stack height =
log-scaled rows, disk radius = log-scaled size, orbiting ring on pgvector
tables, migration obelisk at the pier head) and one container rack per data/
domain on the eastern pier (lit slats = log-scaled disk usage). Clicking a
silo/rack opens a holographic detail card. A down DB renders a pulsing
DB OFFLINE beacon, distinct from a reachable-but-empty database. Fed by
GET /api/city/introspection on a 2-minute page poll.
…ftop kits

The town fabric layer, all derived from the master plan: an octagonal ring
road with spokes to every district and a grand avenue to the harbor (3 merged
draw calls: asphalt / neon edge strips / vertex-colored paint+district pads),
an elevated transit loop with orbiting trams and station pylons, instanced
lamp posts with faked light pools plus plaza holo-trees (4 draw calls at any
density), and deterministic per-app rooftop fixtures (antenna/tank/AC/dish,
seeded like the window textures). Props and trams gate off on the low quality
preset; everything follows cityDayMix for the day/night looks.
- introspection fs section delegates to dataManager.getDataOverview (du/find)
  instead of a third hand-rolled JS directory walk — harbor and Data page now
  report identical numbers, and the slow recursive-stat path is gone
- harbor helper owns ALL pier geometry (decks sized to contain their
  structures, obelisk position, quay offsets) so the component only renders;
  silo colors resolve through the theme palette's getAccentColor instead of a
  drifted private copy; two-row layout deduped; log scaling delegates to
  scaleMetricToHeight; formatCompactCount added to formatters
- transit stops derive from parcel anchors (move a parcel, the tram stop
  follows) with an invariant test pinning the derivation
- cityShowDetail(settings) single-sources the above-low-preset gate used by
  rooftops/trams/street props; WORLD owns the land extent + ground y-stack;
  plaza pad exclusion is a parcel flag, not an id check; gangway width reuses
  AVENUE_WIDTH; instanced-prop matrices no longer rewrite on every re-render;
  introspection poll keeps payload identity via useAutoRefetch compare
atomantic and others added 29 commits June 9, 2026 18:07
refactor([issue-1087]): extract shared useInstallStream hook for SSE install modals
…m flow

Covers settings save (success + error toast), the DB restore-confirm modal
gate (dry-run preview opens the modal; no destructive restore on cancel; real
restore only after explicit confirm; no-dump abort), and pgBackup/degraded
status rendering.
Address claude review (PR #1113): make the Toast mock a callable spy (matching
the real toast's bare-call + namespaced surface) and cover the confirm→restore-
failure branch that toasts an error. Run Now gating split to #1114.
test([issue-1088]): behavioral tests for BackupTab restore-confirm flow
…S_WORKSPACE_ROOTS scoping

Extract the workspace-root allow-list (defaults + PORTOS_WORKSPACE_ROOTS,
symlink-resolved containment) out of routes/commands.js into a shared
lib/workspaceRoots.js helper and reuse it in both routes. detect/repo now
carries a trust-model comment and, when PORTOS_WORKSPACE_ROOTS is set,
confines detection to the configured roots (returns valid:false otherwise);
unset, it stays unrestricted as before.
…-in gate

- detect.js: wrap realpathSync in try/catch so a TOCTOU/permission edge returns
  the route's valid:false shape instead of a 500 (matches commands.js + the
  route's own contract for unusable paths).
- workspaceRoots.test.js: cover WORKSPACE_ROOTS_CONFIGURED + EXTRA_WORKSPACE_ROOTS
  parsing and the configured-roots allow-list fold (the core feature of #1089)
  via stubbed-env re-import.
feat([issue-1089]): document detect/repo trust model + optional PORTOS_WORKSPACE_ROOTS scoping
…ordance

Convert the diff and release-confirmation modals to the shared ui/Modal
component, which supplies role=dialog + aria-modal on the panel,
role=presentation on the backdrop, Escape-to-close, and click-outside.
Add aria-label to the × close buttons and aria-labelledby wiring to the
titles. Adds Modal.test.jsx pinning the dialog roles, labelling, and
Escape/backdrop close behavior.
…Tab call-site test

- Pass align="none" on both modals so the backdrop has no p-4 padding,
  exactly matching the pre-refactor bare-overlay centering (claude review).
- Add GitTab.test.jsx asserting the diff and release dialogs render with
  dialog roles, aria-labelledby titles, a labeled Close button, a
  presentation backdrop, and Escape-to-close — pins the call-site wiring,
  not just the shared Modal (both reviewers flagged the gap).
fix([issue-1090]): GitTab modal dialog roles + close affordance
…t moderates

Adds a qs@6.15.2 override and bumps the brace-expansion override to 5.0.6,
resolving GHSA-q8mj-m7cp-5q26 (qs stringify DoS) and GHSA-jxxr-4gwj-5jf2
(brace-expansion range DoS). Both are transitive, non-breaking fixes
(express/body-parser/googleapis accept the patched qs; brace-expansion 5.0.6
is a patch bump). Drops server npm audit moderates from 11 to 7; the
remaining 7 (pm2 -> ws, kokoro-js -> @protobufjs/utf8) stay blocked on
upstream majors as documented in the issue.
fix([issue-1091]): pin patched qs + brace-expansion to clear npm audit moderates
Adds a Staff↔Piano toggle to the Song reader's multi-part player. Piano
mode renders every selected layer as colored notes falling onto a piano
keyboard and lights each key as its note crosses the hit line — the
aggregate of all checked voices played on the piano at once.

Each layer gets a stable color shared by the falling notes, the keyboard
glow, and a swatch on its layer checkbox. The fall reads the player's
live position() each frame so it stays sample-aligned to the audio.

Keyboard geometry + per-note MIDI mapping live in a new pure, unit-tested
pianoKeyboard.js helper; buildSchedule now carries a midi per note and
createMultiScorePlayer gained position()/duration() accessors.
getBackend() fell back to the file backend under NODE_ENV=test only when
Postgres was unreachable. On a dev machine with a healthy local 'portos' DB
the suite wrote hundreds of fixture universes/series/works straight into the
real database, federated them to live Tailscale peers via
autoSubscribeRecordToAllPeers, and let test-cleanup tombstones delete the
user's real records.

Test mode now short-circuits to the file backend before the health probe.
An explicit MEMORY_BACKEND=postgres still opts a suite into the PG path.
Adds a regression test asserting test mode bypasses a healthy Postgres, and
a one-off recovery script to restore tombstoned-real records, purge fixtures,
and prune the peer-subscription ledger on an already-polluted install.
…pre-roll in the clock

- Clamp the keyboard band to height so a small height prop can't produce a
  negative hitLineY (inverted fall).
- Read draw() via a ref in the resize effect so the ResizeObserver is created
  once per height rather than re-subscribed on every notes/tempo change.
- Add a test exercising the playing=true rAF loop and its cancelAnimationFrame
  cleanup on unmount.
- Keep the LEAD-in pre-roll in the piano-roll clock.
fix(memory): never use a developer's real Postgres under the test runner
feat(songs): Synthesia-style piano-roll view for the layered MIDI player
…zed column

listUniverses/listSeries read deleted-state from each record's `data` JSONB
(`SELECT data FROM universes`), not the row's denormalized `deleted` column.
The recovery script only flipped the column, so 'restored' universes stayed
invisible in the UI (their data.deleted was still true) and a second --apply
reported 'restore 0' because the column no longer matched deleted=true.

Restore now clears data.deleted + data.deletedAt, bumps data.updatedAt, and
resyncs the columns; detection and the live tally key off the JSONB source of
truth.
…eted

fix(recovery): restore the JSONB data.deleted, not just the denormalized column
…ehind cards

The 'Subscribe to peer' (cloud) and 'Share to bucket' popovers were absolutely
positioned inside each series/universe card. Cards establish their own stacking
context (translucent/blur background), so an in-card `absolute z-30` popover is
painted UNDER the following sibling card and is unusable.

Both dropdowns now render via createPortal into document.body with fixed
positioning anchored to the trigger button's viewport rect, re-anchored on
scroll/resize, and the outside-click handler checks the portaled menu too. Also
falls back to host/address/'Unnamed peer' when a peer has no name (was rendering
the literal 'undefined').
fix(sharing): portal peer Sync/Share dropdowns so they don't render behind cards
…e removed googleapis package

The googleapis monolith was dropped from server/package.json in favor of
the scoped @googleapis/* packages, but executeGmailApiAction still did a
dynamic import('googleapis'). Boot survives (dynamic import), so the
breakage only surfaces on the first Gmail archive/delete as a runtime
'Cannot find module googleapis'. Mirror the @googleapis/gmail client
pattern already used in messageGmailSync.js.

Also guard the one-off recovery script's peer_subscriptions prune with an
Array.isArray check (it runs after an irreversible DB commit), and fix a
stale resolveBoomT -> resolveBoom doc-name reference in the city rig.
@atomantic atomantic merged commit b485c1a into release Jun 10, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant