VibeRoller (formerly Mills): a self-hosted team hub: Claude side by side, an end-to-end encrypted Discord-like chat with calls, board, calendar and notes, one shared AI with its own API
  • Rust 56.2%
  • JavaScript 34.2%
  • HTML 7.2%
  • Shell 1.2%
  • CSS 0.3%
  • Other 0.9%
Find a file
VibeRoller hub 96155151d8
All checks were successful
deploy / deploy (push) Has been skipped
Merge develop into main
Verified on the dev hub: a00e976f65
2026-11-08 01:08:03 +00:00
.forgejo/workflows VibeRoller 2.0.0: projects area (sharedwork: new project / pull in a repo, per-person worktrees, worktree.js --project; the shared-folder choice is gone); portal.unityailab.com nginx site (login from inside the server, droids 404 for the rest; the page talks to the hub that served it); VibeRollerAIHost boot service (Windows service + Linux unit, heart/knife start and stop it, installers); Download button above Settings with Vibe Roller Desktop + Vibe Roller AI Host for Windows and Linux (hub-built zips); no-root deploy kit (user services, auto-deploy timer on push to main, pack-data by scp); config.local.json deep merge; R39/R40 rewritten, R136/R139 extended, R140-R142 2026-10-18 20:48:55 -06:00
.github/workflows VibeRoller 1.5.0: the move kit is complete (setup-server.sh for Red, pack-data + restore-data for the data, deploy key locked to one command); no AI host never blocks anything (node offline within a minute, Unity says so at once, chat / board / notes / Work / sign-in keep working; R138); Gee's PC runs the AI node after the move (start-node, autostart + heart node mode, single-instance lock); worktree repair after the host folder rename; pages.yml GitHub-only; R139 2026-10-18 19:29:58 -06:00
bridge T-341: R533-R537 are plain checks now (they pass); the journal is read after its write-behind 2026-11-07 23:42:42 +00:00
deploy The security log is kept with the hub data: deploy snapshots, pack-data, restore and the handoff include bridge/security 2026-11-07 03:29:09 +00:00
native T-343: every Work session, the first one too, can be deleted 2026-11-07 20:38:52 +00:00
web VibeRoller 5.46.1: the chat's @ picker calls Unity again (R527) 2026-11-07 19:08:10 +00:00
.gitattributes Mills: private Claude workspace (Pages UI + local bridge) 2026-10-10 11:25:06 -06:00
.gitignore VibeRoller 5.46.3: ignore bridge/work-sessions/ (the last untracked hub folder that blocked live) 2026-11-07 20:35:56 +00:00
CHANGELOG.md VibeRoller 5.48.0: every Work session, the first one too, can be deleted (T-341) 2026-11-08 00:30:08 +00:00
LAWS.md T-337 / T-338 / T-339: live lends its AI hosts to the dev hub over the bridge 2026-11-07 19:40:32 +00:00
README.md VibeRoller 5.43.0: the desktop app stays signed in (a device sign-in it approves for itself, app only, revoked on sign-out) and frees the GPU (tray: Free the GPU, Image engine on / off; vr-desktop --gpu); Unity's local brain holds the GPU 2 minutes, not 30 (assistant.brainKeepAlive, both hubs); the app's AI host switches find the installed host; R511, R512 2026-11-06 20:59:01 -07:00
versions.json VibeRoller 5.48.0: every Work session, the first one too, can be deleted (T-341) 2026-11-08 00:30:08 +00:00

VibeRoller

Formerly Mills (renamed in 1.3.0). Stored names stay as they were so nothing breaks: the default account mills, the everyone-server's id mills, conversation ids like ch:mills/…, the MILLS_* settings, the X-Mills-* internal headers, and on the first host the Mills folder, Mills-worktrees and the GitHub repo Unity-Lab-AI/Mills. The web address below changes once the new repo is live.

Host folder: on Gee's PC the checkout is now Desktop\VibeRoller (renamed from Mills); the worktrees stay in Desktop\Mills-worktrees. After such a rename, node bridge/worktree.js repair fixes every worktree's pointer, and a Work run heals its own.

Repos: the home is https://git.unityailab.com/GFourteen/VibeRoller (Forgejo); a push to main there goes live on the lab server within a minute. Every release is also pushed to the original Forgejo repo GFourteen/Mills and to GitHub Unity-Lab-AI/Mills (whose GitHub Pages copy is no longer the address). The go-live script pushes develop and main to every remote the checkout has.

A private workspace for a small team: talk to Claude in the browser and have it build and edit files on your own PC, and talk to each other in an end-to-end encrypted chat room. There's nothing to install on your side.

Web app: https://portal.unityailab.com (also https://portal.git.unityailab.com and https://viberoller.git.unityailab.com), the front door on the lab server. Anyone can reach the sign-in page; without a username + password and an unlocked device nobody can do anything (the hub tells anonymous callers nothing but that a hub is here). No tunnel or proxy is needed.

Accounts are created by an admin (see Accounts). The default account is mills; accounts flagged admin get an extra Admin section.

After signing in you land on the main menu: your username in the middle, and the Work and Chat buttons (plus Admin for admin accounts only). Every section has a ← Menu button at the top left.

How it works

Browser (GitHub Pages UI) ──username+password──▶ Cloudflare tunnel ──▶ VibeRoller hub on the host PC (one process, every account)
   │  Work: holds the folder they opened on their PC                         ├─ runs Claude Code (the host's account) in the VibeRoller folder, per account
   │  Chat: encrypts/decrypts messages locally                               ├─ relays Claude's file edits to that person's browser
   ◀── file edits (list/read/write/delete/mkdir) applied in the browser      └─ stores chat ciphertext only

Host: running it

  1. Double-click bridge/start.bat and leave the window open. One window serves every account.
    • Start it by itself, so this machine is always an AI host. On Windows, double-click bridge/autostart-windows.cmd once. It puts a launcher into your Startup folder that opens start.bat minimized every time you sign in. Undo it with autostart-windows.cmd remove, or by deleting VibeRoller.cmd from the Startup folder (Win+R, shell:startup). On Linux, use bridge/viberoller.service, a systemd user service; the steps are at the top of the file, and loginctl enable-linger makes it start at boot before anyone signs in. Starting a second copy is harmless: it finds the hub's port taken and stops without opening a second tunnel. Windows starts it at sign-in, so a PC that waits at the password screen starts VibeRoller once someone signs in. Turn on automatic sign-in if it must start right after boot.
    • VibeRollerAIHost: this PC as an AI host from boot, with a heart and a knife. bridge/host-tools/build.cmd (once) draws the icons, compiles three programs with the C# compiler built into Windows, puts Start VibeRoller AI Host (black heart, start-viberoller-ai-host.exe) and Stop VibeRoller AI Host (knife, stop-viberoller-ai-host.exe) on the Desktop, and offers to install VibeRollerAIHost.exe as a Windows service (install-ai-host-service.cmd, asks for administrator rights once). The service runs the AI node (bridge/ai-node.js) from the moment the PC boots, before anyone signs in, restarts it if it stops, logs to bridge/ai-node.log, and lets signed-in users start and stop it without administrator rights.
      • The knife stops the service: only this PC's AI hosting stops; VibeRoller on the server keeps running for everyone (chat, board, calls, notes, their own AI keys) and Unity says her AI host is offline. It starts again at the next boot, or with the heart.
      • The heart starts the service again (and clears the pause switch). Without the service installed, the two tools fall back to the pause switch bridge/ai-host.off and start-node.bat / start.bat: the knife pauses the AI hosted here (Work on the host's Claude, API answers, Unity), the heart resumes it and starts the node or the hub if nothing runs.
      • On Linux, bridge/host-tools/install-ai-host-service.sh (sudo, once) installs the viberoller-ai-host systemd service, and start-viberoller-host.sh / stop-viberoller-host.sh are the heart and the knife.
      • The same files, ready to unzip on any computer, are the Vibe Roller AI Host download (Download button above Settings).
    • Handing the hub off to the server (bridge/handoff.js): node bridge/handoff.js pack --to ovh makes the running hub pack its own data (accounts with their password hashes and device locks, chat + key, board, notes, pictures, saved sessions, settings), pause its writes (sign-ins, chat, board, notes answer "moved to …"), copy the archive to the server over ssh and restore it there; node bridge/handoff.js switch then writes bridge/.handoff, so from the next graceful restart run.js runs the AI host on this machine instead of the hub, with no tunnel, at every start. undo brings the hub back; status shows the state. A Work run may run it, so the move needs nothing from the host's owner.
    • The first time, it asks for the mills account password if none is set.
    • It starts a Cloudflare tunnel and posts the address to the VibeRoller bridge address gist on the GitHub account gh is logged in to, where the web app finds it automatically.
  2. Requires Node.js, Claude Code (logged in), gh (logged in), and cloudflared (path set in bridge/config.json).
  3. Restarting is safe: passwords live in auth.json files and are only written by set-password.js or the Admin section; device locks live in each account's state.json; sign-in sessions live in bridge/hub-state.json. Nobody is signed out or asked for a new password when the hub restarts.
  4. What Claude may run. allowedTools in bridge/config.json is the complete list of tools a Work run may use without asking (a web session cannot answer a permission prompt, so anything not listed is refused as "requires approval"). Besides reading / editing files it allows exactly the commands the worktree workflow needs: git status / diff / log / add / commit, the regression suite (node bridge/test/regress.js …) and the worktree tool (node bridge/worktree.js …, which also pushes develop, and whose live command ships develop to main and restarts the hub when the person asks). Nothing else runs in a shell. Unity in the chat room never gets any of these. The MCP tool groups a Work run gets are mcp__pc (the person's PC), mcp__board (team board) and mcp__team (team chat + presence). Changing the list takes effect after a hub restart.
  5. The models offered in the Work model dropdown come from models in bridge/config.json (id = what claude --model accepts, label = what the person sees).

Restarts after code or config changes (Claude does this itself)

The window runs bridge/run.js, a small supervisor that owns the Cloudflare tunnel and runs bridge/server.js (the hub) underneath it. After Claude changes hub code or config it runs:

node bridge/restart.js

The hub restarts itself the moment nobody has a Claude reply in progress, under the same tunnel, so the address does not change and the web app shows "VibeRoller is restarting" for a second and carries on. node bridge/restart.js --hard closes the window and relaunches start.bat (new tunnel address, found again automatically) for a wedged hub; --hard --delay 30 does that 30 s later from a detached helper so the Claude run that asked for it can finish its reply; --stop closes it without relaunching. Progress of a hard restart is written to bridge/restart.log. Admins can also press Restart hub in the Admin section.

A fixed address (named Cloudflare tunnel), when the host wants one

By default the hub opens a quick tunnel: a random *.trycloudflare.com address, published to the gist each start and found again by the web app. A named tunnel gives VibeRoller a fixed address on a domain the host owns; everything for it is in place, the host only needs the Cloudflare side:

  1. Once, on the host: cloudflared tunnel login, cloudflared tunnel create mills, cloudflared tunnel route dns mills mills.example.com (your domain in Cloudflare).
  2. In bridge/config.json: "tunnelName": "mills", "tunnelHostname": "mills.example.com". run.js then runs cloudflared tunnel run mills and publishes https://mills.example.com instead of parsing a quick-tunnel address.
  3. On GitHub, Settings → Secrets and variables → Actions → Variables: MILLS_HUB_HOST = https://mills.example.com, then redeploy (push to main). The Pages workflow writes that host into the page's Content-Security-Policy connect-src and its hub-address check, so the page may talk to it; without the variable only *.trycloudflare.com addresses are accepted, exactly as before.

Both kinds work on Windows and Linux; nothing else changes (sign-in, encryption, the gist lookup).

Accounts

Admins manage accounts from Admin in the web app: create an account (a 20-character password is generated and shown once), give someone a new password, reset a device lock, sign someone out, stop their Claude run, delete an account (its folder moves to bridge/profiles-deleted/, nothing is destroyed). Admin can also clear the chat history.

On disk, the mills account is bridge/auth.json + bridge/state.json + VibeRoller/Archive/; every other account is bridge/profiles/<name>/ (config.json: { "user": "<display name>", "admin": true|false, "model": "<default model>" }; the whole bridge/profiles/ folder is gitignored, so names, admin flags and password hashes never leave the host) + VibeRoller/Archive/<name>/. From the command line: node bridge/set-password.js --profile <name> "password" (no --profile = the mills account). node bridge/master-logins.js gives every account a new generated password and writes the whole list (username, password, display name, admin) to Mills-master-logins.txt on the host's Desktop — outside the VibeRoller folder on purpose, so Claude runs in the hub never see it; device locks and sign-ins are untouched. An account whose config.json goes missing keeps signing in with its password (display name = username, not admin) and the hub window prints a warning, so recreate the file to restore it. Everything in bridge/config.json applies to every account unless the account's own config.json overrides it; an account's model is what the Work dropdown's "Default" option means.

Using it

  1. Open https://unity-lab-ai.github.io/Mills/ in Chrome or Edge (on Linux: Chrome, Chromium or Brave; Firefox cannot open folders) and sign in with your username and password.
    • Any screen. Desktop and laptop get the side-by-side layouts; on a phone or small tablet (up to 700 px wide) everything becomes one column: the chat's conversation list is a drawer (☰, closes when you pick one), Work's panels stack, the board shows one column at a time (swipe sideways) and opens tasks as a full-screen sheet, the calendar becomes a day-by-day list, dialogs fill the screen, controls are finger-sized and typing in a box never zooms the page. Opening a folder on your PC still needs a desktop browser.
  2. Work is the Claude workspace. Click Open folder on my PC and pick the folder Claude should work in, then chat with Claude. It creates and edits files in that folder, and you can edit them yourself in the editor panel.
    • Layout. The bar on the far left (☰ widens it to show labels) has a button per panel: Folder, Editor, Claude, Team chat, plus Settings. Each button shows or minimizes its panel; every panel also has a — minimize button. Drag the thin separators between panels to resize them. Team chat opens the chat room inside Work, next to Claude; Pop out turns it into a window you can drag by its title bar and resize from its corner, Dock puts it back. The layout is remembered per account in that browser.
    • Slash commands. Type / at the start of the Claude box and a menu of the common commands opens, filtered as you type (arrows / Tab / Enter pick one, Esc closes). Commands marked to Claude go to Claude Code exactly as typed (/compact, /review, /init, /memory, /cost, /status, /help, /commit, /pr, /explain, /test, /fix, /docs); commands marked here act on VibeRoller itself (/clear or /new = New chat, /stop, /model opus|sonnet|haiku|fable|default, /share, /save, /sessions, /board, /tasks, /chat, /settings). Only the common ones, on purpose.
    • The message box never locks: while Claude is replying, Queue lines a message up for right after this reply (queued messages show dashed; ✕ removes one), and the second button is Stop when the box is empty or Send now when it has text, which stops the current reply and sends your text immediately, ahead of the queue.
    • Work keeps your place (bridge/work-state.js). The conversation on screen and your queue, including a pending Send now, are kept on the hub per person in work-current.json in your profile folder. Every queue change and every new message is saved, so a hub restart, a page reload, closing the browser or switching devices loses nothing. Opening Work brings back your previous conversation with its saved-session title, and Claude continues it. Your queued messages come back and start running. New chat clears it.
    • Sharing your Work session. The Share with everyone button above the Claude messages shares your session with everyone (there is no per-person sharing). The same button then reads Stop sharing; signing out stops it too. Everyone in that conversation gets a pop-up and a Join work button in the chat, which opens a watch-only window of your session: your Claude conversation as it happens (from the last ~150 entries when you started sharing), what you are typing, live, what you send and what is queued, and a dot that pulses while your Claude is working. Watchers cannot type into your session. Each person still works in their own session and their own worktree; open several watch windows to follow several people (drag them anywhere, ✕ to close). Summaries: while you have watch windows open, every message you send to your own Claude gets a short automatic summary of those sessions appended (the latest few exchanges of each, clearly marked as context, not shown in your chat and not shown to anyone watching you), so your Claude knows what the others are doing.
    • Usage bar (just above the Claude messages). 5h shows how long until the current 5-hour usage window resets (hh:mm:ss), 7d the weekly window (dd hh mm), each with a small fill bar and a percentage. These limits belong to the host's single Claude account, so everyone shares them.
    • Percentage: the % used that Claude Code reports. Until it has reported one, the % (shown as ~NN%, greyed and marked "est") is how much of the window's time has passed, not usage.
    • Always in step: the fill, its colour and the % come from the same number. A window shows empty ("reset") once its reset time passes.
    • Fresh numbers: the page re-reads them as each step of a reply arrives, and every 15 seconds otherwise, since other people's runs move the shared limits too. Context is how full this session's context window is (tokens used of the model's window): the bar slides from green through orange and yellow to red as it fills, and turns purple when you are out of usage (context full, or the account hit its limit) — start a New chat then. Hover any part for details.
    • Past sessions. Your Claude conversations are auto-saved (Settings → Auto-save, every 5 minutes by default; 1 / 2 / 5 / 10 / 15 / 30 minutes or Off). The first auto-save names the session itself from your first message plus the date and time, and every later auto-save overwrites that same saved session, so you never have to press anything. Save session saves right away (and asks for a name if it has none yet); once saved, a session also updates itself after every reply. Past sessions lists them: Open (Claude resumes with full memory of it), Rename (or click the session title above the chat), Delete (click twice). The search box at the top searches titles and everything said in your sessions (every word must match, any case); results are ranked by number of matches and show a snippet of who said what, with the matches highlighted. Esc clears the search, then closes the window. They live on the host PC under VibeRoller/Archive/ (the folder name is unchanged) and are never committed.
    • The model dropdown picks which Claude model answers you (Fable 5.1, Opus 5.5, Sonnet 5.5, Haiku 5.5, or your account's default), and below them any Claude of your own or a teammate shared (see "Your own AI"). It applies from your next message (after the current reply), to every new chat, and survives restarts. Each "Done" line shows which model answered.
    • The project picker (top bar) chooses which project Claude works on, always in your own worktree of it (branch work/<username>), so several people can have Claude change the same project at the same time without touching each other's changes. VibeRoller itself is the default project: your worktree is Mills-worktrees/<username> next to the host folder on the first host, /srv/apps/sharedwork/VibeRoller/<username> on the server, created from develop the first time you send a message. Every other project lives in the team's shared projects area ("projectsRoot" in bridge/config.json; /srv/apps/sharedwork on the server): one folder per project, _repo/ = its main checkout with the integration branch checked out (develop when it has one, else its default branch), and one worktree per person next to it. + New project… starts an empty repo there (a README as the first commit, main + develop); ⤓ Pull in a repo… clones an existing one by its https:// or git@host: address (your saved Forgejo token signs you in to the lab's Forgejo; local paths and other git helpers are refused). Switching projects starts a new chat; the old "shared folder" choice (the hub's own checkout) is gone. Ask Claude to "merge" when your change is done: for VibeRoller that is node bridge/worktree.js commit <username> [message] then merge <username> (work/ → develop, pushed to every remote); for another project Claude runs the hub's worktree.js by its full path with --project <name> (commit, merge into the project's integration branch and push it when it has a remote, sync to bring that branch back into work/). Merges use the folder that has the integration branch checked out, or a temporary checkout removed afterwards, so develop stays free to check out in the host folder. node bridge/worktree.js projects lists the projects and who has a worktree; status every worktree; new <name> and clone <url> [name] do what the picker does. Going live (VibeRoller only) is node bridge/worktree.js live --restart (only when asked): it merges develop into main in the host folder, pushes main and develop to every remote, prints the GitHub Pages deploy run, and asks the hub to restart itself gracefully on the new code; it refuses if the host folder has uncommitted changes and leaves nothing half-merged on a conflict. On the server the push to main is enough: the auto-deploy timer fast-forwards the live checkout within a minute (deploy/README.md).
    • Download (the button above Settings in the menu; in the app, the download icon in the user panel) offers the two applications for Windows and for Linux, built by the hub from its own code so they point back at this hub: Vibe Roller Desktop (VibeRoller Chat in its own window: the browser's own install is the best path and the card for it is right there; the download is a launcher for Chrome, Edge or Brave with a Start-menu / application-menu entry) and Vibe Roller AI Host (the AI node with the VibeRollerAIHost boot service, the heart and the knife, a README with the steps). The desktop app stays signed in: sign in once with your password in it and every later start (a reboot too) opens signed in, through a device sign-in the app keeps for itself (only the app, never a browser); Sign out ends it, and it can be revoked under Settings → Connections. Its tray also frees this computer's GPU (Free the GPU unloads the image engine's model and Ollama's models at once) and turns the image engine off / on (on uses engines-start.json in the AI host folder: { "image": { "program", "args", "cwd" } }); vr-desktop --gpu status | free | image-on | image-off does the same from a terminal. Unity's local brain leaves the GPU 2 minutes after her last answer (assistant.brainKeepAlive).
    • Theme opens 23 UI colour themes plus the VibeRoller default, remembered in that browser. Scrollbars follow the theme too: a thin thumb mixed from the theme's accent and line colours, brighter on hover, over a transparent track (Chrome, Edge, Brave, Firefox and Safari, on Windows and Linux alike).
  3. Chat is Discord-shaped and end-to-end encrypted (see Security). The far-left server rail lists your servers: VibeRoller (everyone, always there; its # general is the whole team) and any server someone made with + (name, icon, colour, description, people to invite). Below them: a flag button when you have server invitations waiting (Accept / Decline) and a join button for an invite link or code. Under the picked server: its text channels grouped by category (a lock = private, a flag = announcement), your groups (+ makes a group conversation of up to 10 people; the gear opens rename / add / remove / hand over / leave; only the group's owner removes people), and direct messages with each person (only the two of you can read them). A channel or group message is encrypted for that conversation's members only, and the hub refuses posts, typing, read receipts and calls from anyone else. Unity is in every channel and group (write @unity there and she answers there); she and the team's Claudes carry a BOT badge. A member column (who is in this conversation, online / away / offline, bots) shows in the installed app and on wide screens. Data: VibeRoller/Chat/guilds.json on the host holds names, member lists and server settings only (never message text; a server password only as a salted scrypt hash).
    • Servers work like Discord's (bridge/guilds.js, roles in bridge/guild-perms.js, the window in web/guild-settings.js). Nobody signs up: the hub admin makes every account, so a server is only ever open to people who already have a VibeRoller login. The gear next to a server's name (or right-clicking it in the rail) opens Server Settings, whose tabs show only what your permissions allow:
      • Overview: name, icon, colour, description (needs "manage server"), your own server nickname (shown instead of your name inside that server), leave.
      • Members: everyone with their roles, OWNER and TEMP tags; invite by name (a personal invitation they accept, which skips the password lock), cancel pending invitations, hand out roles, change nicknames, kick and ban (with a reason). You can only act on people strictly below your own highest role, so a moderator can never kick the owner or the admin above them.
      • Invites: invite links that expire after 30 minutes up to 7 days or never, are limited to 1 / 5 / 10 / 25 / 100 uses or unlimited, and can give temporary membership: they leave when they go offline unless someone gives them a role. Copy the link, revoke it any time. Opening a link (…?invite=CODE) offers the join once signed in.
      • Ownership: whoever makes a server or a group owns it (servers and groups from before got their creator as owner). Ownership can be handed on.
      • Roles and permissions follow Discord's model (bridge/guild-perms.js). @everyone has default permissions, and named roles add more; each role has a colour, a position and "show its members separately". Permissions come in Discord's groups: General (view channel, manage channels, make channels, manage roles, view audit log, manage server), Membership (create invite, manage invites, change nickname, manage nicknames, kick, ban, timeout), Text (send messages, send voice clips, read message history, manage messages, pin messages, announce, bypass slowmode), Voice (connect, speak, video / screen share, mute members) and Advanced (administrator). Every server (VibeRoller too, and older servers once the hub loads them) has the built-in levels, highest first: Owner (one person) → Co-owner (as many as the owner names: owner rights under the owner, everything except deleting or handing over the server and managing other co-owners) → Super Admin (administrator) → Admin (manage server, roles and channels) → Super Moderator (ban, manage invites, announce, make channels) → Moderator (kick, timeout, mute in calls, delete and pin messages, audit log, nicknames, slowmode bypass) → Member (talk, voice clips, history, calls, invites, nickname) → @everyone. Each level holds everything below it. Everyone who joins becomes a Member, except temporary members until someone gives them a role. In VibeRoller everybody is a Member. The levels cannot be deleted or reordered; the owner can rename them and change their permissions, except that Co-owner and Super Admin always have every permission. Custom roles start just above Member and can move between the levels (they hop over the built-in spots), and they can start from any level's permissions. Handing the server over keeps the old owner on as a Co-owner, unless they untick it. You can only hand out roles below your own and permissions you hold yourself. On older servers, a role already named like a level becomes that level, and other custom roles move up between Admin and Super Admin so they keep outranking ordinary members. Discord permissions with no VibeRoller feature behind them (stickers, threads, stage channels, webhooks, polls, soundboard, events) are left out. So are the ones that depend on reading message text, like embed links or @everyone, because the hub cannot read end-to-end encrypted messages to enforce them.
      • Role features. Owners, co-owners and anyone with "manage roles" (Admin and above) make roles and give them colours. A person can hold many roles. Their name takes the colour of their highest coloured role, inside that server only: in chat names, @mentions, the @ autocomplete and the member column. Outside it, the person's own colour applies. A role can be mentionable, so @RoleName highlights and notifies everyone who holds it. A role can be self-assignable: members pick or drop it themselves under Overview → Your roles. Such a role may only carry everyday permissions (view, talk, voice clips, history, calls, nickname), so nobody climbs by picking one, but channel permissions can still open opt-in channels to it. Built-in levels are always handed out, never picked. Overview also shows your own roles, highest first.
      • Channels: order (up / down), edit, delete, new. A channel has a display name, topic, category, slowmode (5 seconds to 6 hours), an age-restricted flag (everyone confirms once), can be an announcement channel, and has channel permissions: per @everyone, role or person, each permission inherit / allow / deny, applied in Discord's order (@everyone, then roles where an allow beats a deny, then the person; administrators skip them). "Private channel" is the shortcut: @everyone loses "view channel", and its messages are encrypted only for those who can view it.
      • Moderation: timeout (1 minute to 28 days: they can read but not post, type or join calls), mute in calls (their mic is held off for everyone and their page turns it off), kick, ban (optionally deleting their messages of the last hour, day or 7 days), and delete other people's messages with "manage messages". In the VibeRoller server the hub admins have these as its owner.
      • Settings tab: a welcome channel where the hub posts join and leave lines as "VibeRoller", a rules channel (named in the welcome line), default notifications (all messages or only mentions, for everyone in the server), pause invites, and a new-member wait (5 or 10 minutes before a new member without a role may post).
      • Pins: pin a message from its menu (server channels need "pin messages"; groups and private conversations: anyone in them); a flag button next to the conversation name lists the pins and jumps to them.
      • Calls follow the permissions too: without "connect" the hub refuses the call, without "speak" the mic stays off, and without "video / screen share" the camera and screen stay off. Calls go browser to browser, so the hub cannot cut someone's audio or video. It can only refuse the call and tell their page to switch off.
      • Bans (unban), Security (a password lock: joining by invite link asks for it, 5 wrong tries per 10 minutes), Audit log (the last 200 changes: joins, leaves, kicks, bans, roles, channels, invites, settings), Ownership (hand the server over; delete it). The owner must hand it over before leaving.
      • The VibeRoller server can be renamed and given channels, roles and nicknames like any other. It can have a named owner, who names its co-owners and can hand it on, though it can never be deleted. The hub's admin accounts keep owner rights there as a safety net. Everyone is always in it, so it has no invites, password, kick or ban.
      • Host-set owners. bridge/server-owners.json on the host (gitignored) sets any server's owner and co-owners when the hub starts, for example { "servers": { "mills": { "owner": "<username>", "coOwners": ["<username>", "<username>"] } } }. Each entry applies once, so later changes in Server Settings stick, and editing the entry applies it again at the next start. Unknown accounts are skipped, and the audit log shows the change by "host".
    • VibeRoller Chat, the app. Settings → VibeRoller Chat app detects whether you are on Windows or Linux and installs the chat as its own app from Chrome, Edge or Brave (own window, own icon, Start-menu / application-menu entry): it is this same page in app mode (index.html?app=1), chat only (servers, channels, groups, private messages, calls with theater and screen share, Unity as a bot), signed in with your VibeRoller account; Work, Board, Calendar and Admin stay on the site. Technically a Progressive Web App: web/manifest.webmanifest + SVG icons (web/icons/) + web/sw.js, a service worker that keeps the app shell opening on a slow connection and never caches hub answers. Nothing to build or download from anywhere else, and it runs the same on both operating systems. App mode hides the site's headers, so the app has a user panel at the bottom of the side bar: your picture or initials, your name in your colour, your status, Settings (profile, picture, app settings, theme, connections) and Sign out. The app follows your theme like the site does. Every change to the site goes to the app too (hub law 11): new page files go into the service worker's shell list, with its cache name bumped, so installed apps pick them up. Chat features must be reachable in app mode, app-mode styles may not hard-code colours, and nothing may depend on Windows or Linux alone. The regression suite checks this (R131). Red numbers count unseen messages per conversation, on the Chat button in the menu and on the Team chat button in Work; the tab title shows the total. Key fingerprints are under Key fingerprints at the bottom of that list.
    • Presence is hub-wide, not just the chat. Everyone signed in is online (green), away (yellow: tab hidden, or 5 minutes without input) or offline (dim, with "last seen"), and the others can see which part of VibeRoller they're in. The hub counts each page's open connection, so closing a tab, losing the connection or signing out shows as offline within seconds and coming back shows at once. It appears as a dot and status line under each person in the chat list, as "N online" in the chat header, and as a Who's here strip on the main menu. "Sam is typing…" appears under the messages for the conversation you're looking at (a private conversation shows it only to the other person); While Unity composes an answer the chat shows the same animated "Unity is thinking" bubble Work shows for Claude. Typing lapses by itself after a few seconds.
    • Motion. New messages slide in, buttons react to the hand, the sign-in status has a live pulse, and Work shows a "Claude is thinking…" bubble until the first words arrive. Everything respects the system's reduced-motion setting.
    • Markdown in both chats (Claude's replies in Work and the team chat), rendered by web/md.js:
      • Text: headings, bold, italic, strike, ==highlight==, code, single line breaks kept.
      • Links: http / https / mailto only, including bare URLs.
      • Structure: nested bullet, numbered and task lists; tables with column alignment; quotes and GitHub alerts (> [!NOTE], TIP, IMPORTANT, WARNING, CAUTION); --- rules; footnotes.
      • Code blocks: language label, syntax colouring and a Copy button.
      • Images: ![alt](https://…) and inline data: images load (lazily, without a referrer; click opens the full size). An http:// picture is tried over https (the page may only load https) and falls back to a link. A relative path such as ![shot](out/shot.png) is loaded from the folder you opened on your PC in Work (nothing is uploaded; without an open folder the markdown stays as written) — so Claude can show you a picture it just made in your project.
      • Math: $$ … $$ blocks, ```math fences and inline $…$ render with VibeRoller' own copy of KaTeX (web/vendor/katex.min.js + css + woff2 fonts, vendored and integrity-checked by node bridge/vendor.js like Mermaid, loaded only when a formula appears, trust off); a formula KaTeX cannot read stays as its source with the error in its tooltip, and without the library the source is shown. Prices like $5 and $10 are left alone.
      • Safety: the renderer never inserts HTML, so raw HTML in a message stays plain text.
    • Icons. The site uses no emoji. Every icon is a custom SVG from one sprite at the top of web/index.html: 57 line icons drawn in the text colour, so they follow every theme, plus the VibeRoller mark, drawn with a gradient of the theme's own accent colours. The sprite is hidden at zero size, not with display:none, because gradients inside a display:none SVG do not paint.
    • Look and feel (web/polish.css, loaded after the page's own styles). It adds depth, surfaces and motion built only from the theme's colour variables, so every theme gets them: a soft accent glow behind the page, gradient primary buttons with a sheen, quiet ghost buttons that tint on hover, sunk input fields with focus rings and theme-coloured select arrows, glass headers, menus, toasts and dialogs (blurred backdrops, lifted panels, a soft entrance), a sign-in card with the VibeRoller mark, a main menu of tiles (icon, name, one line on what is inside), a server rail with a gradient active pill, highlighted channels, gradient badges, lifting board cards and a profile card with a sheen. Channels show drawn icons (# for text, a lock for private, a megaphone for announcements, people for groups), and the rail shows a mail icon for invitations and a link for joining. Pins use a pin, owners a crown. Motion follows the system's reduce-motion setting. Layout, sizes and breakpoints stay in web/index.html. Script adds them with ic(name) / label(el, name, text). The gear on Claude's tool lines comes from CSS.
    • Diagrams. A ```mermaid block draws a diagram (with Source and Copy). It uses VibeRoller' own copy of Mermaid in web/vendor/mermaid.min.js, loaded only when a diagram appears, in strict mode, with the SVG cleaned before it is shown. To install or update it, run node bridge/vendor.js from the VibeRoller folder (or node bridge/test/regress.js --install-vendor, which does the same and then runs the regression suite; that form is one of the commands Claude may run from a Work session): it fetches Mermaid 11 from the npm registry, verifies the package against the registry's published integrity hash before unpacking, writes web/vendor/mermaid.min.js, and records the version and sha256 in web/vendor/VENDOR.json; then commit both files. node bridge/vendor.js --check verifies the files on disk, and regression check R102 does the same. Without the file, diagrams show their source with a note.
    • YouTube links posted in the chat (youtube.com/watch, youtu.be, shorts, live) get a player right under the message, so the team watches in the chat (privacy-enhanced embed, no autoplay, up to three per message). The page's security policy allows that one frame host and nothing else.
    • Click a message to Reply (your message shows a quote that jumps back to the original), and on your own messages Edit or Delete (click twice). Edited messages say "edited"; deleted ones leave a "message deleted" marker. Times are shown in your own browser's time zone (hover for the full date).
    • Receipts. Under your messages: "seen alex · unseen sam" — who has looked at that conversation since. Any message's menu shows the same for it.
    • @mentions. Type @ and pick a person (Tab / Enter takes the first match), or write @username / @nickname. Mentions of you are highlighted.
    • Sounds. Three different tones: a normal message, an @mention of you (or a private message to you), and a reply to one of your messages. Each has its own volume slider in Settings. No sound plays for the conversation you are looking at.
    • Text to speech has a master switch. Settings → Text to speech turns everything VibeRoller says out loud on or off at once: chat read-aloud and Unity's spoken replies. Below it, Unity speaks her replies is the same setting the Voice button in the Unity screen toggles; both now follow your account instead of one browser.
    • Read aloud (optional). Turn on "Read new chat messages aloud" in Settings and new messages are spoken one after another ("Alex: …", "Alex privately: …", "Alex replied: …") with your browser's built-in voices, so you can keep working and just listen. Settings has speech volume, speed, voice (with ▶ to try them before saving), whether to read every message or only @mentions / private messages / replies to you, and a button to stop and clear the queue. Code blocks and links are not read out; messages in the conversation you are looking at are not read.
    • Dictation (🗣). Next to Send in the team chat and in the Claude box in Work. Click it and talk: your words are typed into the box after whatever is already there, so you can stop, fix something, and click again to carry on. It stops by itself after about 3 seconds of silence (or click it again, or start typing). It never sends on its own — press Send / Enter — with one exception: finish with the words "send message" and then pause; the button turns green and the message is sent (without those two words). Speaking again during that pause cancels it. Uses the browser's own speech recognition (Chrome, Edge; not available in Firefox, where the button is hidden). Note: in Chrome and Edge the audio is turned into text by the browser maker's speech service, so dictated speech leaves your computer that way; the resulting text is sent end-to-end encrypted like anything typed.
    • Voice clips. Press 🎤 next to Send to record (the browser asks for microphone access the first time; the microphone is switched off again as soon as you stop). Up to 2 minutes; Send clip or Cancel. Anything typed in the box goes along as a caption, and a clip can be a reply. Clips are encrypted end to end exactly like text and play inline with a player; Settings has a voice clip volume and Auto-play incoming voice clips, which plays them one after another in the same queue as read-aloud (the stop button clears both).
    • Voice / video calls (page side web/calls.js, hub side bridge/calls.js). 📞 Call (top of the chat) starts or joins the call of the conversation you are in: # Everyone has one shared call, each private conversation its own two-person call. There is no ringing: when someone is in a call you get a notification (one soft tone, a pop-up you can click to join, and a "… is in a call here — Join" bar in that conversation). Joining uses your microphone (listen-only if you do not allow it); the camera always starts off and is switched on and off with 📷 even when the browser already allows it (the camera light goes off when you turn it off). Mute / Unmute (or the M key when you are not typing) switches your microphone, Leave hangs up. Whoever is talking gets a green glowing ring around their tile (you too). Each person's tile has its own 🔊 / 🔇 and volume slider (hover the tile): they only change what you hear, are kept in your browser per person, and multiply with the call volume in Settings; closing the tab or signing out leaves too. While the chat is open (Chat screen, or Team chat in Work with its side list shown) the call sits at the top of the chat's side list; otherwise it is a small window you can drag anywhere, and it snaps to the nearest corner or edge when you let go.
      • Theater. Press Theater in the call (or click a shared screen) and the call takes most of the window with big tiles while the chat stays on the left (the conversation list tucks away); Esc or the button leaves it. It is remembered in that browser. On a phone the call sits above the chat instead.
      • Screen share. Share screen asks the browser which screen, window or tab to share and sends it as an extra, full-width tile ("'s screen", whole picture visible, not cropped) that everyone in the call sees; your own camera keeps its tile. Stop with the same button or the browser's own stop bar. Technically a third sender on every connection (audio, camera, screen, in that order) with its own stream id, so sharing never renegotiates the call; the hub only learns the on/off flag. Works wherever the browser offers getDisplayMedia (Chrome, Edge, Firefox on Windows and Linux; not on phones).
      • End-to-end encrypted. Audio and video go straight between the browsers over WebRTC (DTLS-SRTP). The hub only passes on the connection set-up, and those messages are encrypted with the same per-person chat keys, so the hub can neither listen in nor put itself in the middle. Compare key fingerprints once to be sure nobody swapped a key.
      • Codecs. Opus for voice (≈32 kb/s, error correction for lossy networks, near-silent when nobody talks); video prefers VP9, then AV1, VP8, H.264. All are decoded by the browser itself. Settings → Video quality: low 320×180 / medium 640×360 (default) / high 1280×720.
      • Microphone processing (Settings, used for voice clips and calls): noise suppression (on by default), echo cancellation and automatic level, each switchable; changes apply to a call in progress. Settings also has the call volume.
      • Networks. Browsers find each other with a public STUN server (Google's by default: it learns each browser's public IP address, never any audio or video). On very strict networks a direct connection is impossible; the host can then add a TURN relay to "iceServers" in bridge/config.json (standard WebRTC format; TURN only relays encrypted media). Calls are a mesh (everyone connects to everyone), meant for a small team.
    • Pop-ups. While you are in Work, new messages float up in the bottom-right corner and fade away (click one to open it). Turn them off in Settings.
    • Settings (main menu, the chat header, and the Work bar) has three tabs, so what others see about you stays apart from how VibeRoller behaves for you:
      • Profile (user settings, everyone sees them; "Save profile"): nickname (shown with your messages and usable for @mentions), your colour (16 preset colours that stay distinct from each other and adapt to every theme; "auto" picks one from your username), a status line, pronouns, an about me bio (300 characters, https links clickable), location, up to 3 links, a banner colour (by default it follows your colour), and your local time (pick a time zone, or "use this browser's"; empty = not shown). A live preview shows your card as others see it. The default profile is just your name in your colour, with your initials as the picture. A profile picture can be uploaded or removed at the top of the tab. Your browser crops it to the middle square and shrinks it to 256 px before upload, so the original and its metadata never leave your PC. The hub accepts PNG, JPEG or WebP only, checks the type by the file's first bytes (no SVG, no disguised files), caps it at 300 KB, and keeps one picture per person in VibeRoller/Avatars/ (gitignored; deleted with the account). The picture shows on your card, in the member column (with your presence dot) and next to your name on your messages.
      • App settings (private to you, saved to your account so they follow you to every browser; "Save app settings"): sounds and volumes, pop-ups, text to speech, microphone processing, call volume and video quality, voice clips, auto-saving Claude sessions, and the theme. The theme picker in the header saves the theme to your account too. A browser that signed in before this keeps its theme, and the account adopts it.
      • Connections: API access, the VibeRoller Chat app, My AIs and My Forgejo.
    • Profile cards. Click a name, either on a message or in the member column, to open that person's card. It shows their banner, name in their colour (inside a server, their highest coloured role's colour and their server nickname), @username and pronouns, status, about me, location, local time, links, their roles in this server, and a Message button. Your own card has Edit profile. Profiles are stored with each account on the host (state.profile, next to the app settings in state.prefs; bridge/user-profile.js).
    • Unity is in the room too (on the host's local brain by default, "roomAgentBrain" in bridge/config.json = a Claude model id to switch back). When she's called she uses the caller's own tools, time and place, can post pictures, and her sign-in hello includes your local time, weather and local headlines.
    • How she is called: She says hi when someone signs in. Otherwise only her own handle calls her: write @unity (any capitalisation; @claude or just "unity" no longer do). Your browser only encrypts a message for her when it contains @unity (or is a private message to her), and then sends the 5 messages before it in that conversation along inside the encryption — so she reads at most the last 6 messages and the hub cannot read the rest of the chat. She answers in the room in the lab's Unity persona. A private message to her always gets a private answer that only you receive. It works in the host's VibeRoller folder, so it can also work on VibeRoller itself when asked. The hub won't restart while Claude or anyone else is in the middle of a reply. Set "roomAgent": false in bridge/config.json to remove it, "roomAgentModel" to choose its model, and "roomAgentName" to rename it (default Unity).
  4. Board is the team's task board, shared by everyone and by Claude. The ideas come from HyperProductivityManager.
    • Columns: Backlog → To do → Doing → Review → Done. Drag a card between or within columns, or press + on a column to add one.
    • More than one board: pick one in the header, or add one with + Board. The header also has a filter (title, label, person, id) and Mine (assigned to you or claimed by you / your Claude).
    • A task has a markdown description (the same renderer as chat, including tables and mermaid), labels, priority P1–P5, a due date, an assignee, a repeat (daily, weekly, every 2 weeks, monthly, quarterly, every 6 months, yearly), and its own activity log.
    • Repeats: completing a repeating task moves it to its next due date instead of finishing it. Monthly repeats are month-end aware: Jan 31 → Feb 28 (29 in a leap year) → Mar 31.
    • Claims. Before working a task, a person (Claim 30 min) or Claude takes a time-limited claim. While it's held, nobody else can edit, move, complete or delete the task. Renew it to keep working; release it or complete the task when done.
    • Lapsed claims. A claim that isn't renewed lapses on its own, so a crashed or forgotten run never blocks work. Lapsed claims are flagged in the header and on the card.
    • No silent overwrites: saving a task that someone changed since you opened it is refused, and you reload first.
    • Deletes leave a tombstone in the board file, and every change goes to an activity log (the newest few show in the footer, next to who has the board open).
    • Claude works the board too. Every Work run has board tools (list, read, create, claim, renew, update with a progress note, complete, release). Claude acts as " · Claude" and is told to claim before working. Its claims show on the board in green, and even you can't edit a task your own Claude is holding until it releases it.
    • Give to Claude (on a task, or in Work's Tasks pane) puts a ready instruction for that task in Claude's box in Work.
    • Delegate (on a task) hands it to a teammate in one step: pick the person, write a hand-over note; the note lands in the task signed by you and the assignment shows in the activity log.
    • Due reminders. A task assigned to you that is due today or overdue gives you one reminder a day: a tone, a pop-up you can click to open it, and a browser notification if you allowed them.
    • Activity. The footer shows the latest changes; click it for the whole log (who did what, when; click an entry to open its task).
    • My day (Board and Calendar headers) is the planner view HyperProductivityManager was built around: your open tasks (assigned to you or held by you / your Claude) as Overdue · Today · This week · Later · No date yet, each with Done, Tomorrow and Next week (move the due date), Open and Claude. Plan with Claude puts the whole list in Claude's box in Work with a request to read the calendar, propose an order and re-date what cannot be finished.
    • Keyboard. / focuses the board filter; Ctrl+1 / Ctrl+2 / Ctrl+3 switch to Board / Calendar / Work from anywhere (not while typing).
    • Claude plans with the board and calendar too. Besides claim / note / complete, every Work run can create tasks with a due date written as text (today, tomorrow, +3d, next friday, 2026-10-20, none), read the calendar (board_calendar: tasks by day for a range, plus everything overdue), move tasks between columns, re-date or clear due dates, make a new board, and delete a task when asked (a tombstone stays). Ask it to "plan next week", "move everything overdue to Monday", or "put a review on Friday" and it edits the same board you see.
    • Claude can talk to the team. Every Work run also has the team tools (bridge/team-mcp.js → bridge/team.js): team_say posts into the team chat as " · Claude" (to # general, privately to one person with to, or into a channel / group with conv), team_channels lists the servers, channels and groups you are in, and team_presence tells it who is online, away or offline. It can also make a channel (team_channel_create) or a group (team_group_create), see the wider picture (hub_status: who is where, who is in a call or sharing a Work session, the host account's 5-hour / 7-day usage windows), search your own past Claude sessions (sessions_search) and list the AIs you may use (my_providers). Posts are sealed with the hub's own chat key (the one Unity uses), so every member can read them; the " · Claude" label and the person's colour travel outside the encryption. Unity never answers these posts. Claude is told to announce in the chat before any hub restart and when something ships.
    • Live for everyone through the same long poll as the chat. The menu's Board button shows how many tasks are due today or overdue for you.
    • My Forgejo (Settings). Everyone can paste their own Forgejo access token (made on Forgejo under Settings → Applications, with repository + issue read / write). The hub checks it against Forgejo, shows "Connected as ", keeps it on the host in bridge/forgejo-users.json (gitignored) and never sends it back to any page; Replace and Remove are next to it. With a token saved: your board changes reach Forgejo as you (issues, closes, comments under your name), and your own Work runs get FORGEJO_URL / FORGEJO_TOKEN for Forgejo's API plus a git extraHeader for the Forgejo host (through GIT_CONFIG_COUNT, nothing written to any git config), so their pushes and fetches there authenticate as you. Nobody else's run gets your token. The Forgejo address is forgejo.json's url, else config.json's forgejoUrl, else https://git.unityailab.com.
    • Forgejo mirroring (off until set up). When the host writes bridge/forgejo.json ({ "url": "https://git.unityailab.com", "repo": "UnityAILab/VibeRoller", "board": "team", "pollMinutes": 2, "token": "<optional hub token with issue rights>" }, gitignored), the tasks of that board are issues on Forgejo and back: a new task becomes an issue (title, body, a "VibeRoller task T-n" marker), renames and body edits follow it, completing closes it, moving it back reopens it, deleting closes it with a comment; the other way, every couple of minutes an issue someone made on Forgejo becomes a task labelled forgejo, closing it completes the task, reopening puts it back in To do, and title / body edits land on the task. Each task remembers the issue's last change it has seen, so nothing ping-pongs. Outgoing changes use the token of whoever made the change (My Forgejo), else the hub token, else they are skipped; reading Forgejo changes back needs the hub token, because that background job belongs to nobody. Cards and the task show the issue number as a link; the footer says which repo the board mirrors. Forgejo has no plugin system (as ProjectTaskManager found), so this is an outside service over its REST API, which survives Forgejo upgrades.
    • Where it lives: board data is stored on the host in VibeRoller/Board/board.json (never in git). It is not end-to-end encrypted, because Claude has to read it, the same as Work runs.
  5. Calendar is a month view of every task with a due date. As in HyperProductivityManager, a task with a due date is the calendar entry, so the board and the calendar can't drift apart. Click a day to add a task due that day, or click a task to open it on the board. Today is highlighted, overdue tasks are red, finished ones are struck through, and repeating ones show the repeat icon.
  6. Notes (menu) are the team's notes: markdown files in folders on the host (VibeRoller/Notes/, never committed), shared with everyone and with Claude, as in HyperProductivityManager where notes are real .md files you can also open, grep and back up outside the app. Left: the folder tree with a search box (every word must match the path or the text; results show a snippet); right: the note with Edit / Preview (the same markdown renderer as the chat, tables and mermaid included), Save (Ctrl+S), Rename (move with /), Delete (twice). Saving sends the time you opened the note, so if someone saved it meanwhile you are told to reload instead of overwriting them. Names are case-insensitive on purpose (Windows and Linux agree): a second Plan.md next to plan.md is refused. Every Work run has notes_list (with search), notes_read, notes_write and notes_delete, acting as " · Claude", so you can ask Claude to write up a meeting, keep a decisions log or draft a how-to. Notes are not end-to-end encrypted (Claude reads them), like the board.
  7. Work → Tasks pane (rail button Tasks): what you or your Claude hold, what's due within a week (yours or unassigned), and what's assigned to you. Each task has Claude (hand it to Claude) and Open (jump to it on the board).
  8. Unity (menu) is Unity as your own assistant. Everything runs on the host PC that runs start.bat; you only need the browser.
    • Ask her to add / list / finish tasks (priorities, due dates, notes), add calendar events, set reminders ("remind me in 20 minutes…"), remember things about you, keep a knowledge base, do math, tell the time. Your tasks, calendar, reminders and memory are yours alone (kept in your account's folder on the host).
    • Anything that deletes something shows a Yes / No card first and nothing happens until you answer.
    • Due reminders pop up in the bottom-right corner wherever you are in VibeRoller (and as a browser notification if you allowed them).
    • Time and place are yours: your browser sends its time zone with every message, so "what time is it" and due times are your local time. 📍 Share location adds your rough position (rounded to about 100 m) for local answers; click again to stop.
    • The dropdown picks her brain: Unity (local) is her own model on the host's GPU (Ollama, unity-brain), or any Claude model (uses the host's Claude account). New chat starts over; your data stays.
    • Pictures. Ask for a picture or a selfie and she makes it on the host's own image engine (bridge/image-engine.py: local Stable Diffusion, started by the hub with Python 3.10 + torch + diffusers, checkpoint assistant.imageCheckpoint). Selfies are written fresh from the moment (what is going on, where she is, who is with her, the weather where you are) with her look (assistant.selfieLook) added after the scene. Long prompts are encoded in pieces, so nothing is cut; there is no negative prompt. Pictures show inline (click to download); private ones only for you, room ones for everyone.
    • Photos. 📷 takes a webcam photo, 📎 sends a picture from your PC (shrunk to 1024 px in your browser). It goes with your next message: she sees it (her model has vision) and can change it: edit (restyle it, put you somewhere else, keeping your face) or inpaint (repaint one area, the rest stays). Your photos and her edits are private to you.
    • Voice. 🎤 records your mic (click, talk, click again); the host turns it into text (bridge/speech-engine.py, faster-whisper on the CPU, model assistant.whisperModel) and sends it. 🔊 plays her replies in her own voice with the emotion she picked (unity-voice-v2 on the host, assistant.voiceDir, started by the hub). Both are per browser and nothing is installed on your side.
    • Web and media. She can search the web (Bing results, no key), read a public page to answer from it or summarise it (the host never fetches its own network or private addresses for this), and play YouTube: the video plays right in the chat (privacy-enhanced embed). Works in the Unity section and with @unity in Chat.
    • Your own computer. She can list, read, write, make folders in and delete (Yes / No first) files in the folder you opened in Work ("Open folder on my PC"); your browser does it, the host never touches your PC. She can also run short JavaScript or Python programs in a sandbox in your browser (a Worker: no page, no cookies, no files; Python via Pyodide, loaded on first use) and read their output. Nothing runs on the host. Admins switch both per account in Admin (Unity: PC files / Unity: Run code); they are on unless an admin turns them off.
    • Weather and news where you are: your rough place comes from your connection (or 📍 Share location for the exact one); weather from Open-Meteo, headlines from Google News, no keys.
    • Host setup: Ollama installed. The hub starts ollama serve itself and loads unity-brain from its Modelfile once (assistant.modelfile in bridge/config.json, default ../Backrooms/models/unity-brain/Modelfile next to the VibeRoller folder). Her persona text is bridge/assistant-persona.txt on the host (never committed); her look in pictures is assistant.selfieLook.
    • With the hub on the server, the AI host does that on its PC (T-293): the native host starts an installed Ollama that is not running (autoDetect.startOllama in ai-node.json, on by default) and loads unity-brain into it from a Modelfile it finds (unity.modelfile, or by default next to the hub checkout work.hubRepo/../Backrooms/models/unity-brain/Modelfile, in the host folder's models/unity-brain/Modelfile, or next to the host folder's parents). The hub asks the host whose Ollama has the brain when several lend one; until a brain is loaded anywhere, Unity answers on a stand-in chat model of that Ollama (the Unity screen says which) instead of "my brain glitched".
  9. Workflows (menu; 5.26.0, T-184) are Hyper Workflows: re-usable node-based flow charts you design by hand (with an AI side chat that explains, suggests and draws nodes on request) and run from VibeRoller, Hyper Prod Man and Jcode PGM. A prompt sent through one is carried by the engine through the whole automated back-and-forth (AI calls, JEV decisions, programmatic steps, approvals that wait for you) until it ends; the AI is one kind of node inside it. The spec is the team note plans/hyper-workflows.md.
    • The editor: palette, drag and snap, wires between typed ports, sections with their own model, sub-graphs you can open, undo / redo, a property panel, a test run with live node states, versions with a message on publish, diff and restore, duplicate, categories and search. Custom node kinds can be made on the fly (from a selection, or as http / script kinds) and shared with the team once reviewed. Laws and saved prompts are defined once and applied to nodes, sections or whole workflows.
    • Running: /hyperwork <name> [--project P] <prompt> (short /hw) in the Work box, with a type-ahead as you type the name (grey = cannot run here, with the reason); the run's steps follow in Work, an approval shows Approve / Reject, the answer comes at the end. The same line works in Hyper Prod Man's chat and in Jcode PGM (which can run workflows, not change them). A Work run's AI can run one too (hyperwork_list / hyperwork_run). Triggers run a workflow on a timer (every N minutes … yearly), on a Forgejo / GitHub webhook or an internal event; standing workflows keep their run history, can be paused, and tell their owner in the chat when a run fails.
    • Scopes and rights: a workflow is yours, a team's (config hyperworkTeams), a group's or the company's; run / view / edit / publish / admin rights per workflow; secrets for HTTP steps come from config hyperworkSecrets and never appear in a run log. Data lives in Hyperwork/ next to Board/ (gitignored). Everything here is the Rust hub's.

Is my message being worked on?

Every message you send shows a status line under it. Work: Queued (waits for the current reply), Claude is working on this… 1 min 20 s, then Done in 42 s, Stopped or Failed (with the reason). Unity section: Unity is working on this… then Done, Stopped or Failed. Chat, a message with @unity: Sent to Unity, waiting…, Unity is answering… while she types, Unity answered once her reply is in, or No answer from Unity yet after 3 minutes.

Your own AI, and an API key for every person

Gee's Claude Code login on the host answers everything by default, and nothing below changes that: the host runs exactly as before. On top of it:

  • My AIs (Settings). Anyone can hook up an AI of their own and keep it private or share it with the team: an Anthropic API key, a second Claude Code login on the host (the host runs claude login once with CLAUDE_CONFIG_DIR set to a folder of their own and enters that folder here), or any OpenAI-compatible endpoint (base URL + key + model ids). Shared ones appear for everyone. Claude-capable providers show up in Work's model dropdown as their own group, and a Work run then runs claude under that login / key (your usage, not the host's, and the shared 5h / 7d bars ignore it). An OpenAI-compatible provider cannot run Work (Claude Code needs Claude) but serves the API below. Providers live in bridge/providers.json on the host (gitignored, secrets inside; never delete it).
  • API access (Settings). Every person can make their own key (mills_…, shown once, kept as a sha256 hash) for the hub's OpenAI-compatible API: base URL = the hub's address + /v1 (shown in Settings), GET /v1/models, POST /v1/chat/completions with or without stream. Point any OpenAI-speaking harness at it and it talks to VibeRoller; answers come from the provider you pick there (the host's Claude by default, or one of your / the team's AIs), and an optional model. Rotation: rotate any time (the old key dies at once), revoke, or set the key to expire after 1 / 7 / 30 / 90 days so it must be rotated. One request at a time per key; a request is a stateless claude -p run in an empty folder (VibeRoller/Api/sandbox, gitignored) with no tools, so a key can never touch files or the team's data. A regression check drives the whole path against a fake Claude and a fake endpoint.

On a server (the lab box), with the AI from home

deploy/README.md is the step-by-step move of the hub onto the lab server, where every push to main on Forgejo then deploys it. Three scripts do the work: deploy/setup-server.sh (root, every server step, idempotent), deploy/pack-data.cmd / .sh (the old host's data, hub stopped, nothing reset) and deploy/restore-data.sh (the server). The people steps that remain are Gee's Claude login on the server, the Forgejo secrets, and Gee's approval of the data move.

No AI host never blocks anything. The hub stays up on the server whether Gee's PC is on or not. With his PC off or paused, only Unity's engines are missing: the hub notices within a minute, Unity says "my AI host is offline" at once in her section and in the chat, and chat, board, notes, Work's saved conversations, sign-in and everyone's own AI keys keep working. Work on the server uses Gee's Claude login there, so it works without his PC too.

  • The address. VibeRoller lives on the lab server behind nginx at https://portal.unityailab.com (also portal.git.unityailab.com and viberoller.git.unityailab.com). The sign-in is the protection: username + password + device lock, scrypt hashes, a pause after repeated wrong passwords, nothing told to anonymous callers. The page the hub serves always talks to the hub that served it, whatever name reached it. deploy/README.md has the layout and the steps.
  • Server mode. bridge/config.local.json (gitignored) is laid over bridge/config.json key by key (nested objects one level deep, so assistant.via can be set alone), so a server keeps its own settings and its git checkout stays clean. deploy/config.local.example.json is the server's version:
    • publicUrl is the fixed address. The page the hub serves learns it, and that origin may call the API; allowedOrigins lists the other names that reach the hub.
    • projectsRoot is the shared projects area (/srv/apps/sharedwork), worktreeRoot where the VibeRoller worktrees go.
    • tunnel: false means no Cloudflare tunnel.
    • behindProxy: true means a request forwarded by nginx counts as coming from outside, so anonymous status stays minimal and the client's address comes from X-Real-IP.
  • AI hosts: the AI from home, nobody's login on the server. A machine runs the VibeRoller AI Host (bridge/ai-node.js) and lends its AI to the hub: its Claude Code login (Work runs and API answers for the people it allows happen on that machine, the hub relays the words), its local models (OpenAI-compatible endpoints such as Ollama, carried through the host), Unity's engines (Ollama brain, image, voice, speech), and direct computer access to a folder for its owner's Work runs (the native app instead of the browser's open folder).
    • Identity. Every host has a UUID, a secret and a nickname the hub issues (adjective-animal, like silly-panda; renameable). Settings → Connections → My AI hosts: New host, rename, remove, who may use each provider, and the AI Host download with the host's config already inside. Registry on the hub: bridge/ai-hosts.json (gitignored). The older "aiNodes": { "<name>": "<secret>" } config still works (the name is the id).
    • How it connects. The host connects out over HTTPS (no port opens at home): hello (what it offers; both sides derive one session key from the secret and two nonces, its id vr-… shows in Settings), then a long poll for jobs. Work jobs and their events travel sealed with that key on top of TLS. The hub sends engine names and paths, never addresses.
    • Providers and sharing. ai-node.json lists the host's providers (claude-cli with an optional configDir, anthropic key, openai endpoint), each with models, allowedUsers and caps (max5h, max7d, default 90 %). They show up in everyone's AI pickers as nickname · label (owner); the hub refuses a run past a cap until the window resets. Gee shares only to the group (mills, sponge, gee).
    • Work through Jcode PGM on a host (opt-in, native host, 5.10.0). Off unless you add it: a provider { "id": "jcode", "kind": "jcode", "label": "…", "models": ["…"], "jcodeCommand": "/path/to/jcodepgm", "jcodeProvider": "<jcode profile, optional>", "jcodeArgs": [] } (or "harness": "jcode" instead of the kind) together with "allowWork": true. The host then says work-jcode in its hello caps, the hub lists the provider as "AI host · Jcode PGM" for the host's owner only (Work runs on the host's own computer), and each Work turn runs jcodepgm run --ndjson in that person's worktree on the host with a private jcode home per person under the host folder's jcode/<person>/jcode-home (the team MCP servers as mcp.json, the hub's system prompt as the prompt overlay; no auto-update, no telemetry, no auto-poke). Without a jcode provider (or without allowWork) the host says hello exactly as before. It does not answer API calls.
    • Default. The hub's config.local.json sets "defaultProvider": "host:gee" (Gee's online Claude host is everyone's default) and "hubClaude": false (the server has no Claude of its own). Settings → AI source picks host → provider → model per person; the Work dropdown shows the same.
    • AI only, never the computer. With a host's Claude chosen, Claude Code runs on the hub (the server) in your own worktree in sharedwork; only its model calls go to the host through the hub's local relay (/llm/<token>), and the host adds its own login and forwards them to Anthropic (bridge/host-relay.js: api.anthropic.com/v1/messages for Claude, chatgpt.com/backend-api/codex/responses for ChatGPT / Codex, nothing else). The login stays on the host; no file there is touched. A host refuses work and file jobs unless its own config says "allowWork": true. ChatGPT / Codex (codex-relay) answers the API and Unity; Work uses Claude. node bridge/handoff.js probe on a host tries each shared login once. The host restarts itself when its code or config changes.
    • Running it. The VibeRollerAIHost service runs the node from boot (bridge/host-tools/install-ai-host-service.cmd on Windows, install-ai-host-service.sh on Linux; see "Host: running it"). By hand: bridge/start-node.bat (Windows) or bridge/start-node.sh (Linux); a second copy finds the first and closes. With bridge/ai-node.json present and no service, bridge/autostart-windows.cmd makes the node start at sign-in instead of the hub (the service installer removes that launcher: it is not needed any more).
    • Stopping and starting. The knife stops the service (or, without one, pauses the node with ai-host.off), so the hub sees the node offline within a minute and Unity says her AI host is offline; the heart brings it back. Streamed answers (Unity's brain, local models, Claude's events) are forwarded piece by piece as the host's engine produces them, so words arrive as they are made.
    • Getting it onto a computer. The Download button above Settings (on the site, and in the app's user panel) offers Vibe Roller AI Host for Windows and for Linux: the node, the service, the heart and the knife, with a README and an ai-node.example.json that already names this hub. An admin hands out the node name + secret (aiNodes in the hub's config.local.json).
    • What it doesn't cover. Work runs need Claude Code and the files, so on the server they run there on Gee's Claude login. They don't go through a node.

Versions

VibeRoller uses semantic versioning (MAJOR.MINOR.PATCH) for the project as a whole and for every module (hub law 14). versions.json at the root holds the project's version and one per module: every bridge/*.js, web/*.js, web/*.css and web/index.html. CHANGELOG.md has a dated section per project version with Added / Changed / Removed / Fixed lists.

  • MAJOR: something stops working the old way, such as an incompatible hub route, a stored file that needs migrating, a changed message or encryption format, or a removed feature.
  • MINOR: something new that leaves the rest working, such as a feature, route, setting or permission.
  • PATCH: a fix or polish that adds nothing new.

Every change bumps each module it touches, and the project by the largest of those bumps, with a changelog section in the same commit. The changelog records every change (hub law 15): any tracked file, including README, docs, tests and workflows, not only modules. A change that touches no module still moves the project's PATCH version. Sections run newest first, with Added / Changed / Fixed / Removed / Security lists written for the team, and old sections are never rewritten. R133 checks it. The regression suite (R132) refuses a module that differs from the production checkout without a higher version, and a project change without a higher project version and its changelog section. Settings → App settings shows "VibeRoller x.y.z", and the module versions when opened, on the site and in the VibeRoller Chat app. /api/check returns them too. The first versioned release is 1.0.0.

Publishing updates

The app is published from main (GitHub Pages deploys web/). Work follows Git Flow: feature/<name> from develop, merged --no-ff into develop, then develop into main. The Pages workflow fills in the gist owner from the MILLS_GIST_OWNER repository variable (Settings → Secrets and variables → Actions → Variables), so no account handle lives in the source. Never commit auth.json / state.json, bridge/profiles/, bridge/hub-state.json, Archive/, Chat/, logs or .claude/ (all kept out by .gitignore).

Security

  • First sign-in claims an account. The first browser that signs in to an account becomes the only one allowed into it. To move someone to a new browser, an admin uses Reset lock in Admin.
  • Nothing without a sign-in. A visitor who finds the address gets the sign-in form and nothing else: every API route answers 401 until a session token is presented (the hub checks the token before any route is even looked at; a regression sweep hits every route named in the hub source without a token to prove it), the only anonymous answers are the sign-in itself and a bare "a hub is here" status (through the tunnel it carries no process details), unknown paths, folder listings and path traversal are 404, and the page ships with every screen but the sign-in form hidden.
  • Passwords are stored on the host PC only, as scrypt hashes. After 5 failed sign-ins for a username, that username is blocked for 15 minutes. Only the GitHub Pages origin may call the hub. The Admin endpoints check the account's admin flag on every request.
  • Chat encryption. Each account has an ECDH P-256 key pair created in its browser. The private key stays in that browser and is also backed up on the hub encrypted with a key derived from the account password (PBKDF2, 310,000 rounds), so it follows the account to a new browser. Every message is encrypted with a fresh AES-256-GCM key, which is wrapped separately for each member. The hub (and its disk, VibeRoller/Chat/) only ever holds public keys, the locked backups and ciphertext, and each reader is sent only their own wrapped key. Limits worth knowing: the hub sees your password for a moment when you sign in, so someone who controls the host PC while you sign in could unlock your backup; compare fingerprints in the member list once with each other to rule out a swapped key. A password reset done by Admin on someone who then signs in from a new browser starts a fresh key: older messages stay unreadable in that new browser. What the hub can see without reading any message: who sent what when, who a private message is for, whether a message was edited or deleted, and the receipts (how far each person has read each conversation). Deleting a message removes its ciphertext from the hub's disk. Exception: Claude. Its private key is kept on the hub (VibeRoller/Chat/agent-key.json) so it can read and reply, which means the host PC can read every message sent while Claude is a member.
  • Message format v3 (all new messages and edits). Three extra protections against a hub that tampers with stored messages:
    • Bound to sender, recipient and id. Each message carries a random 16-byte id. The sender, the recipient (or none for # Everyone) and that id are authenticated data on the body, and each member's wrapped key is also bound to that member's name. If the hub moves a message to another sender, conversation or reader, it fails to decrypt.
    • Replays refused. The hub refuses a second message with an id it has seen. Browsers also refuse a copy of an id under a different seq, and once a sender has used v3, they refuse later old-format messages from that sender (downgrade guard).
    • HKDF wrap keys. Wrap keys come from HKDF-SHA256 over the ECDH secret with a fixed label, not the raw secret.
    • Padded length. The plaintext is padded to a size bucket (256 bytes, doubling up to 64 KiB, then 64 KiB steps), so the ciphertext length no longer shows how long a message is.
    • Older messages still decrypt the old way.
  • Message format v4 (browsers with CompressionStream; older ones keep sending v3):
    • The message JSON is deflate-compressed before encryption (length prefix + deflate + padding). The hub only sees compressed, padded ciphertext, and decryption and decompression happen only in browsers.
    • A voice clip's audio is a separate ciphertext with its own key. That key travels inside the encrypted message, so editing the caption keeps the clip. The message list no longer carries clips: a clip is downloaded (/api/room/blob, only for that message's readers) and decrypted when someone presses play.
    • Voice and images are already compressed formats, so they are not deflated again.
  • Key pinning. Each browser remembers every member's public key the first time it sees it. If the hub later hands out a different key for someone, they are marked ⚠ CHANGED under Key fingerprints: nothing is encrypted to that key, and messages sent with it stay hidden until you compare fingerprints with them outside VibeRoller and press Trust new key.
  • Page and hub hardening:
    • The page has a Content-Security-Policy: it may only talk to itself, the hub's Cloudflare tunnel and the GitHub API, with no outside scripts, frames, plugins or images.
    • The hub address read from the gist is accepted only if it is a *.trycloudflare.com address. The last good address is cached, so a page load skips the GitHub lookup.
    • Hub answers carry nosniff, no-referrer and X-Frame-Options: DENY, and API answers are no-store.
    • Password hashing runs off the main thread, so sign-ins never stall anyone else. The 5-attempt limit also counts parallel attempts.
  • Faster and in sync:
    • The chat is pushed by a long poll: the hub answers the moment something changes, instead of every browser asking every 3 seconds.
    • In calls, each person's audio and video are sent as one stream, so the browser lip-syncs them.
    • ICE candidates are sent in batches, so connecting needs fewer round trips through the tunnel.
  • Claude can't run shell commands by default. allowedTools / permissionMode in bridge/config.json control that.