You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Weft asks to turn on Project Sync for the project you're working on. Once a thread in a project reaches Review or Done, a "Turn on Project Sync for ?" dialog explains that synced projects keep their cards on your web dashboard while they run on your computer, so you can manage your Weft from the web, and that teams can watch each other's work and contribute in real time. It offers Sync, Not now and a Don't show me again box. It asks at most once a launch and never about a project that already syncs. Settings → General → Reset tips brings it back. (components/SyncPromptDialog.tsx, lib/sync-prompt.ts, lib/use-sync-prompt.ts)
A feedback dot in the board's corner. A small dot at the right end of the footer opens a chat where an AI asks what's in your way, then a few short follow-ups. Once it has enough (or you click "That's all"), it writes a summary for you to check. Send puts it in the Weft team's Slack, signed with your Weft account when you're signed in, and keeps a copy in ~/.weft/feedback/. Built in components/FeedbackDot.tsx, lib/feedback.ts, lib/feedback-send.ts and app/api/feedback.
🏅 73 new badges, Weft explorer and secret ones. The Wefter Profile now has 143 badges in 17 families: Speed, Calendar, bigger Milestones and Streaks, Tokens & spend, Craft, Board, Projects, Leagues, Social, Undeterred among the Limits, the ❓ Secret ones (hidden until you earn them) and Weft explorer, which logs your first use of the vault, the webhook relay, a dev server, a pane screenshot, a card or app share, your profile and every feature intro. Old badge ids are unchanged, so nothing announces again. League and social badges read the new badge_facts() RPC on the league timer (lib/badge-kit.ts, lib/extra-badges.ts, lib/*-badges.ts, lib/explore.ts, lib/badge-facts.ts).
Super admins can now live in the database, not just in SUPER_ADMIN_EMAILS. A new public.super_admins table (user_id, email, created_at) lists accounts that see the Admin pages and rail icon, so adding one needs a row, not a redeploy. A signed-in account can read only its own row and nobody can write to the table through the API, so no one can add themselves. The env list still works as the bootstrap, and the gate checks it first. Any lookup error reads as "not an admin". Files: lib/admin.ts, supabase/migrations/20261025171511_super_admins.sql, docs/architecture/admin.md.
The card that asked for a restart now stands out. When a thread called mcp__board__restart_weft_when_idle, Weft showed one red pill in the header — but the card that asked looked exactly like every other card, and it wasn't recorded anywhere at all. The asking card is now remembered in the restart drain (RestartStatus.requestedBy) and marked on the board: a thin red ring with a soft glow, and a ⟳ Restart asked pill at the start of its title, drawn like the header's pill so the card and the pill read as one thing. The card keeps its own layout: no side bar, no wash, nothing squeezed beside its picture. Files: lib/restart-tool.ts (the tool names the card that asked), lib/restart-when-idle.ts (the drain keeps it), lib/types-card.ts (RestartStatus.requestedBy), components/RestartMark.tsx + components/restart.css (the mark), components/CardView.tsx and components/Board.tsx (the card draws it).
Weft's server can read Slack now, not just post. The Slack rail could only send: the bot token stayed sealed in Electron main, so the server had exactly one way in — post this message. Anything that needed to read from Slack had no door at all. There's a new control route, POST /slack/call, that makes any Slack Web API method with the token main already holds (conversations.history, auth.test, anything), and POST /api/slack/call in front of it so the server can use it. The token still never leaves Electron main, and the server still never sees it. The app manifest also declares the incoming webhook the deployed Weft posts feedback through, and the docs now spell out that Slack has no Web API method for incoming webhooks — the feature is turned on by the manifest or by hand, and the URL is shown once. Files: electron/slack.cjs, app/api/slack/call/route.ts, lib/slack-manifest.json.
The board as a sheet. A toggle in the project bar swaps the columns for a list: every card on one table, a line each, with the header stuck to the top and the columns lined up down the sheet. The columns are title, status, stage, tags, folder and age; the status reads Needs you for a card waiting on you, and the folder carries a +N when other cards share its working tree. The colour lives in the group heading rather than a bar on every row. Which view you last used is remembered. On a narrow board the sheet drops the roomiest columns — the tags and the folder, then the age — instead of squeezing the title away, because it answers to the width of the board itself rather than the window. Files: components/BoardList.tsx, components/CardRow.tsx, components/list-grid.css, components/ViewToggle.tsx, lib/board-prefs.ts, lib/use-view-mode.ts, lib/board-view.ts.
Agents: pick who a thread is, starting with Weft's copywriter. The New thread dialog has an Agent menu beside the model menu. 🛠 Builder is Weft's own soul, the one every thread had until now: it plans, builds, tests and ships. ✍️ Copywriter writes in Weft's voice (the home page, feature intros and voiceovers, changelog entries, posts, UI copy) and keeps every tool a thread has, so it can read the product, show drafts in the browser pane and put the copy where it lives. Right-click a card → Agent: … to switch it between turns: the conversation carries on under the new soul. A ✍️ in the card's footer marks a copywriter, a cron keeps its source card's agent, and threads and the copilot can start one with create_card's agent. The copywriter's soul is Weft's style guide: Weft as the subject, "your AI", the payoff in a "so" clause, no plumbing and no hype. lib/agents.ts, lib/weft-prompts.ts, components/NewCardDialogControls.tsx, lib/card-menu.ts, docs/architecture/agents.md.
Feedback can reach Slack without anyone ever pasting a webhook URL. Slack has no Web API method for incoming webhooks — you cannot create, list or fetch one — so the only way to get the feedback channel's address is for a person to install the app and pick a channel. Weft now does that itself: /api/slack/oauth/install sends you to Slack's channel picker, Slack redirects to /api/slack/oauth/callback, and the callback exchanges the code with the app's Client ID and Secret and writes the webhook straight into public.slack_webhooks. The collector reads it from there, so the address is a credential that never passes through a chat, a log line or a page — the only things you supply are the app's ordinary OAuth credentials. Files: lib/slack-oauth.ts, lib/slack-webhook-store.ts, app/api/slack/oauth/install, app/api/slack/oauth/callback, supabase/migrations/20261026040000_slack_webhook.sql, lib/feedback-send.ts.
Crons in the list view. The sheet ends in a ⏰ Crons band with a row per cron. Each row shows whether the cron is on and when it runs next, its schedule in words, whether each run forks a thread or starts fresh, its folder, when it last ran and how many runs it has made. Click its status to run it now or switch it on or off, and unfold it to see its run cards, each one openable. Elsewhere in the sheet, a run carries a violet "⏰ run N", and the thread its runs fold under says "⏰ next in 3h" (or "off"). "⏰ cron" in the Tags filter cuts the sheet down to the crons and the threads they come from. Files: components/SheetCrons.tsx, lib/list-crons.ts.
Type to search, Space B to add, drag to reorder in the list view. In the list, just start typing and the sheet searches titles from your first letter. Space then B jumps to the Backlog's add row (unfolding it) so you can type a title right away, and Space then R starts a thread. Board shortcuts that are single letters move behind Space in the list: Space N for New thread, Space T for the terminal. J and K still move through the cards. Backlog rows drag into order by their row number, as on the board. Dragging is off while the sheet is sorted. Files: lib/list-keys.ts, components/use-list-keys.ts, components/use-sheet-drag.ts, components/BoardList.tsx.
Each kind of update can have a channel of its own. The Slack drawer had one channel per project, so every commit, deploy and card status from a project landed together and there was nowhere else to put them. Under Channel per kind each kind — commits, pushes, deploys, card status, growth — can now have a channel of its own, and a kind with one goes there whatever project it happened in: the kind's channel is the most specific answer, so it beats a project's channel and the default. No channel puts a kind back to following the project. Files: components/SlackRoutes.tsx, components/SlackSettings.tsx, lib/slack-route.ts, lib/slack-store.ts, lib/slack-types.ts, lib/use-slack.ts.
Growth lands in Slack as it happens, and a summary goes out every morning. A new user, a new team and every payment now post to the growth channel. The numbers come from production Supabase — a new user is a row in auth.users, a new team is an org with more than one member (every sign-up makes a personal org, so counting orgs would count sign-ups), and a payment is a billing event with subscription.synced filtered out so one subscription doesn't count five times. Weft remembers where it got to in a watermark beside the rest of the Slack settings, and it only moves after a successful post, so nothing is posted twice and nothing Slack refused is lost. The first look goes back a week, so a channel switched on today still says something about the recent past. Once a morning a summary goes out with the headline numbers and the growth dashboard's address; it says plainly when it did not go out, rather than claiming a message that never arrived. Files: lib/growth-read.ts, lib/slack-growth.ts, app/api/slack/growth-summary/route.ts, supabase/migrations/20261026043000_growth_events.sql.
The repo now measures itself every week. A new cron runs the repo-analysis skill over the codebase each Monday morning — repo shape, import graph, docs drift and commit history — and writes a report to ~/.weft/show/repo-analysis/, comparing the new numbers against the previous week's so the report leads with what moved rather than what is. The first run found the changelog pairing rate sitting at 26% (377 user-visible changes in 30 days shipped undocumented), an 11-cycle import graph including a 33-module loop through the runner, and 32 broken file references across the architecture docs, 13 of them in teams-vault.md alone. Files: docs/architecture/skills.md (the skill and what each script measures), docs/architecture/crons.md (the two crons Weft runs for itself), docs/architecture/MAP.md (the index entries).
Slack and ⏰ Crons have their own intro videos. They were the last two drawers on the left rail that opened without one. Now each plays a short narrated intro the first time you open it, like every other drawer. Slack's shows Weft posting your commits, deploys and card updates to a channel, /weft starting a card, and a reply in its thread going to your AI. Crons' shows how to pick a schedule, each run landing as its own card under its thread, and the drawer's row with Run now. In public/intros/slack.html and public/intros/crons.html, their voices beside them, and lib/feature-intros.ts.
Changed
One menu picks the model and the CLI, in the New thread dialog and in Settings. The Claude Code / puku-cli / Codex / Goose button bar and Settings → Models' separate "Default CLI" select are gone: every model menu is the same one, with a heading per CLI, each starting with that CLI's own default, and picking a model moves the thread to the CLI that serves it. Goose lists only the models a saved provider key serves. The permission mode picker is now a menu that says under each mode what it does (Auto, Accept edits, Ask me, YOLO). The model menu also lost its box-in-a-box look, and picking Codex's default now really starts the thread on Codex. (components/ModelSelect.tsx, components/ModePicker.tsx, components/NewThreadDefaults.tsx, components/NewCardDialogControls.tsx, lib/use-new-card-dialog.ts, lib/goose-provider.ts)
New Wefters sign in with GitHub on first launch, after the welcome video and before adding projects. A signed-out first launch now has a Sign in to Weft screen between Get ready and the projects. It says the account keeps your Wefter Profile and turns on Project Sync, share links, leagues and the free Puku trial, and that your code and threads stay on your Mac unless you turn on Project Sync. Next waits until you've signed in. If you're already signed in (including from the tour's profile beat), or sign-in isn't set up on this Weft, the screen doesn't appear. For anyone who is still signed out, the Project Sync prompt is now "Sign in to Weft?", with what an account adds and Sign in with GitHub, and it waits for a card to reach Review or Done instead of opening while a thread waits on a question. On Get ready, when no Claude Code, puku-cli or Codex login is found, puku-cli's row offers Try free. Once you're signed in, it reads "Ready · free trial on your Weft account" and your first thread runs on puku-cli's Puku trial instead of failing on a Claude Code login. (components/WelcomeAccount.tsx, components/WelcomeDialog.tsx, components/WelcomeFooter.tsx, components/WelcomeIntro.tsx, lib/welcome-trial.ts, components/SyncPromptDialog.tsx, lib/sync-prompt.ts, docs/architecture/welcome-dialog.md)
A feature intro pauses when something comes on top of it. If an intro video is playing in its modal, a drawer or a dialog and an achievement pops up or you open the New thread dialog, the video and its voice stop, ▶ shows over it, and a click plays it on from where it stopped. Every dialog opening on top does the same. The dialog an intro plays in never pauses its own intro. (lib/intro-cover.ts, lib/use-feature-intro.ts, components/Modal.tsx, lib/use-board-dialogs.ts, public/intros/player.js, docs/architecture/feature-intros.md)
A thread can open a new site with your Touch ID instead of being refused. Before, a thread that asked the browser pane for a site you hadn't opened in that card's pane got a flat refusal ("Open … in the pane first"), and you had to type the address yourself. Now Weft asks you with Touch ID ("open www.google.com in a card's browser pane"), or with a confirm dialog on a Mac without Touch ID. A yes adds the site to that card's list, so the thread can use it from then on. A no still refuses, and Weft asks again next time. Several calls for the same site share one prompt. Files: electron/pane-origins.cjs (authorizeThreadOpen), electron/browser-pane.cjs.
The list view works like a spreadsheet. The sheet sits on frosted glass with grid lines and numbered rows. Click a column's header and start typing: the Title filters cards as you type, and every other column narrows its values as you go (↑↓ to pick, Enter keeps only the match, an Only button on each value). Age flips newest ↔ oldest with one click. Bands fold on a click, and the Backlog comes first, folded to start with. A card folds open to show its subagents and the cards it started (cron runs included). Click or right-click a card's status to stop, run, continue, restart or park it. Click its folder to move it to another, with a type-ahead picker. To add cards, type a title at the foot of the Backlog and press Enter; nothing starts, and the cursor stays for the next one. Typing into Running starts a thread, and the "+" on a band or in the corner opens it all up. Titles underline under the pointer. Files: lib/list-sheet.ts, lib/list-tree.ts, lib/list-actions.ts, lib/typeahead.ts, components/BoardList.tsx, components/SheetHeader.tsx, components/SheetFilterMenu.tsx, components/SheetQuickAdd.tsx, components/CardRow.tsx.
Superadmin Users works like the board's list view. The Users page loads every account and shows them as a sheet on glass, with grid lines and numbered rows. Type anywhere to search name or email. Click Provider, Joined or Last seen to filter by value with type-ahead (Today, This week, This month…). A click on Joined or Last seen flips newest ↔ oldest, and the page remembers your sort and filters. A row still opens that account's activity drawer. Underneath, the list view's sheet is now its own package, @aimhuge/sheet in aimhuge/components (v0.9.0), so any list of rows in any AimHuge app can be a sheet. Weft keeps only its columns: lib/list-sheet.ts for the board, lib/users-sheet.ts for Users (docs/architecture/sheet.md). /api/admin/users now hands out up to 200 accounts a page.
A thread that commits a feat or fix without a changelog entry is reminded to add one. Half of Weft's own feat and fix commits used to ship with no entry (280 of 511 in the 30 days to 26 Oct). The note Weft adds after every commit now asks a card that has never recorded an entry in that project to call changelog_add, and names the sha to stamp the entry on, so the change reaches CHANGELOG.md. The repo-analysis scripts now count changelog entries edited inside the commit too, and list load-time import cycles apart from type-only ones (lib/changelog-stamp.ts, lib/changelog-git.ts, plugin/skills/repo-analysis/scripts/).
No intro video starts on its own any more, and ⌘N shows a tip instead. Intros used to play with their voice the first time you opened a drawer, the code quality dialog, the leagues or New thread. Now each one plays only when you click ▶ beside the title. The pane beside the New thread dialog always shows the splash, with a tip under it that changes every 8 seconds: a shortcut you may not know, or a drawer's intro with ▶ Watch to play it right there. (lib/new-thread-tips.ts, lib/use-feature-intro.ts, lib/use-new-thread-pane.ts, lib/show.ts)
Picking Codex or Goose now tells you they skip Weft's guards. The model menus' Codex and Goose headings read "· no Weft guards", and the New thread dialog adds one line while one is chosen: no secret guard, folder gate or Bash hooks run for them. The FAQ says the same. lib/unguarded.ts, components/ModelSelect.tsx, components/BackendPicker.tsx, components/NewCardDialogControls.tsx.
Fixed
Esc closes the SuperAdmin pages. Pressing Esc on /admin (Users, Billing, Coupons, Settings) goes back to the board, like closing a drawer; in a text field it only blurs the field. The admin bar also lost its duplicated Billing and Coupons links. app/(admin)/admin/AdminEscape.tsx, app/(admin)/admin/layout.tsx.
Shared data apps load their Supabase reads on weft.zip. The hosted read route expected supabase: { fn }, but every weft-app.json writes supabase: "growth_overview" with args beside it, so each Supabase read on a weft.zip/a/<id> link failed with "Function name undefined isn't a Postgres identifier". It now reads the same shape the Mac does and sends the manifest's args as defaults (lib/app-read.ts).
Card pictures on the commit walk-through page load for real now. The pane could read card attachments only at 127.0.0.1, but the walk-through names its pictures localhost (that's how Next reports its own origin), so every one 403'd into a broken image. The pane's read-only attachment pass now covers both loopback names on Weft's port, still GET/HEAD on /api/attachments/* only. electron/api-token.cjs.
A project whose folder has a space in its name (like Google Drive's ~/My Drive/app) no longer asks about every command. The folder gate split each Bash path at the first space, so cd "/Users/you/My Drive/app" read as ~/My, which is outside the project, and nearly every command got an "Outside the project" prompt. A quoted path now runs to its closing quote and \ escapes a space, so the gate sees the real folder. A sibling folder still gets asked about. (lib/folder-guard.ts, docs/architecture/thread-guards.md)
The New thread intro's poster finally says something, and the splash's timing can't drift. The splash poster's one-liners live in a hand-kept table that had fallen one behind — the New thread intro was missing from it, so its poster opened on an empty line, and it was missing from the verifier's list too, so its video was never checked. It now has a line of its own ("Start a thread, and give it a plan") and is verified like the rest. The splash's lead (how long the poster is up before the story starts) was written out in four files — the page, the renderer, the encoder and the verifier — so retiming the poster would have left the voice delayed by the old number and the verifier expecting the old length, with nothing saying so. It lives in one file now that all four read, and the pre-commit hook no longer skips lint on plain .js, so a change to the player is checked.
The feedback dot names itself, and your feedback actually arrives. The little purple dot in the board's corner now shows a Feedback label when you hover or focus it, so it reads as a control instead of decoration. The bigger fix was behind it: a summary you sent never reached the team, and said it had. The collector's table was never applied to the live database, so every insert failed, and SLACK_FEEDBACK_WEBHOOK_URL was never pushed to Vercel, so nothing could post to Slack — but the collector answers 204 when either half works, so the chat showed "Sent to the Weft team" every time. The public.feedback table is applied now (supabase/migrations/20261025181607_feedback.sql), and the docs say what to check when a summary reaches weft.zip but no channel. The Slack half still needs an incoming webhook for the feedback channel in the 🔐 vault, pushed to Vercel. Files: components/FeedbackDot.tsx, components/FeedbackDot.test.tsx, supabase/migrations/20261025181607_feedback.sql, docs/architecture/feedback.md.
A puku thread is no longer held behind Claude's 5-hour limit. The New thread dialog would refuse to send and read "Start at 2:30am" when Claude Code's 5-hour window was spent, and changing the model in the composer did nothing — because the limit was never the model's. Weft had been treating Claude Code and puku-cli as one counter pool on the premise that both read the same Anthropic subscription; they don't: Claude Code reads Anthropic's account windows, while puku-cli bills through Puku's gateway and has its own allowance. Each CLI now reads its own plan, so a window answers only for the CLI that read it: a puku thread starts even while Claude is out of room, Claude's reset no longer releases a card parked on Puku's allowance, the burndown chart draws each CLI's own history, and the status strip shows each plan's own numbers. A per-model cap (the model's own allowance, as opposed to the plan's) is now also kept out of the plan log, so it gates that model alone instead of every model on the plan. (lib/plan-pools.ts, lib/plan-limits.ts, lib/limit-reset.ts, lib/limit-history.ts, lib/limit-resume.ts, lib/limit-kind.ts, lib/use-start-gate.ts, lib/runner-turn.ts, app/api/limits/route.ts, app/api/limits/plans/route.ts, components/LimitBurndown.tsx, components/StatusStrip.tsx)
A model's reset time is read on the right day now. The limit message names its reset as a local time with a zone ("resets 10:40pm (Asia/Bangkok)"), and Weft read that as the machine's own calendar day, so a reset was filed a day early or late whenever the zone's date differed from UTC's — and "resets 1:00am" could resolve to a time that had already passed. The calendar day is now read in the zone the CLI named, and the roll-forward re-derives tomorrow in that zone instead of adding a flat 24 hours, so a reset is the time the message actually named. The model picker and the thread starter show the right hour, and a reset that falls exactly now still rolls forward to the next one. (lib/model-limit.ts)
The Slack manifest asks for the feedback webhook the way Slack actually reads it. The manifest declared incoming_webhooks as { name: "Feedback" }, which is not the shape Slack reads — it takes { incoming_webhooks_enabled: true } — so applying it would have left the feature off with nothing to say why, and the webhook could never be turned on. It now also asks for the incoming-webhook scope as a user grant: that is the grant whose OAuth answer carries incoming_webhook.url, so the webhook's address can be read programmatically instead of copied off a console page that shows it only once. A test pins the shape, the scope's side, and the bot scopes the rail needs. Files: lib/slack-manifest.json, lib/slack-manifest.test.ts, docs/architecture/feedback.md.
🎨 Settings shows your own theme behind it. If you picked a theme you made, Settings used to show Twin Suns behind its pages. Now it shows your theme's picture. (lib/use-scene.ts, components/SettingsDialog.tsx)
Turning on Slack token rotation could not have broken the app — but it would have broken every future sync. Slack's answer to a manifest that disables token rotation once it is enabled is cannot_disable_once_enabled, so a manifest still saying false fails validation outright and the manifest never lands — and the failure reads like a syntax error, not like "you enabled a one-way switch in the console". lib/slack-manifest.json now matches the app, and a test pins it so the next person cannot quietly turn it back off.
The board doesn't panic when a linked package's stylesheet is imported. The @aimhuge packages are linked rather than copied — package.json points at link:../components/packages/ — so the files they resolve to live outside Weft's folder, and Turbopack refused to run its CSS loader on a path outside its roots ("... is outside configured roots"). Importing @aimhuge/sheet's stylesheet in app/globals.css took the whole dev server down: every page and API route answered 500 and the log filled with the same panic. The two linked packages' folders are now declared as Turbopack roots, so their stylesheets compile like any other. (next.config.ts)
Goose threads work: they run with your saved key, show their replies, remember the conversation, know Weft's rules even behind Puku, and use Weft's own tools. Every Goose turn ended with "Goose exited with code 1 without finishing the turn": Electron main handed the Goose key routes a getter as if it were Goose itself, so every key grant threw, and the server quietly started Goose with no key. Once Goose ran, its streamed replies didn't show (only "Done."). Each turn was a fresh session, so it forgot the thread. Puku's gateway replaced its system prompt, so it never saw SOUL or the BOARD STATUS rules. And it had none of Weft's tools, so it opened links in the system browser instead of the pane. Now the grant works and a failed one is logged. Replies are gathered and filed whole. Later turns pass --resume. Behind Puku, a new session's first message carries Weft's instructions. Each turn gets Weft's MCP servers (mcp__browser__open_url and the rest) through the card's own Goose config, with a per-turn token removed after. A turn that dies shows Goose's own error text. The launch helper starts the goose binary, waits for its output to drain, names a signal that stopped Goose, and writes ~/.weft/goose/<card>/state/weft-launch.log. A message starting with ! goes to the model instead of the shell. The Goose tests no longer write into ~/.weft. (electron/goose-ipc.cjs, electron/goose-env.cjs, lib/goose-runner.ts, lib/goose-mcp.ts, lib/goose-reply.ts, lib/goose-runner-events.ts, lib/goose-recipe.ts, lib/goose-provider.ts, lib/goose-state.ts)
Parallel scoped coverage runs no longer collide.pnpm test lib/x.test.ts --coverage used to send every run's report to one shared temp folder, so two at once (two cards, or a card and its subagents) failed with "Something removed the coverage directory". Each run now gets its own mkdtemp folder under SCOPED_COVERAGE_ROOT, removed when vitest exits; the whole-repo coverage/ is still left alone. Main files: scripts/run-tests.mjs, scripts/run-tests.test.ts, docs/architecture/quality.md.
A crash mid-save can no longer lose the board.board.json is now written to a temp file, fsynced and renamed over the old one, so a crash or power cut leaves either the old board or the new one, never half of each. lib/atomic-write.ts, lib/store.ts.
Replying from the weft.zip board no longer freezes the thread. Sending a message from the web board used to send the thread into an endless re-render loop. The message you just sent is now drawn once and the thread stays live (lib/use-pending-sends.ts). The same fix gets CI green again: vendored minified bundles are skipped by lint, and the CI check job now stops after 20 minutes instead of hanging.
weft.zip's pricing cards now sell what billing delivers. Team now charges for the people actually in the workspace. Adding someone, accepting an invite or removing a member updates the Stripe seat count, prorated, so a growing team is no longer under-charged. Each pricing card now has a button: Free downloads the app, and Get Pro and Get Team go through /billing (signing you in first if needed) to your workspace's billing page, where the upgrade dialog opens and its button starts Stripe Checkout. The BlastCP tiers are off the cards, the upgrade dialog and the Open Source copy, because BlastCP doesn't apply them yet. Main files: lib/billing/seats.ts, lib/team-ops.ts, app/invite/[token]/actions.ts, lib/billing/plan-link.ts, app/billing/page.tsx, components/home/Plans.tsx.
Security
The Slack install no longer needs the app's Client Secret. Weft's incoming-webhook install asked for the app's Client ID and Secret, and the Secret is the one value a person has to copy by hand out of a console page. The app now uses PKCE: the authorize step sends a challenge instead of the secret, and the exchange sends back the verifier that made it. The secret is never needed for the exchange at all — the install needs only the public Client ID, and the Client Secret, if you set one, is now just the key that signs the install state. The state carries that verifier and a timestamp under an HMAC, so a tampered or replayed install link is refused, and it expires after 15 minutes. The app manifest asks for it (pkce_enabled), and the manifest validates against the live app. Files: lib/slack-pkce.ts, lib/slack-oauth.ts, lib/slack-manifest.json, app/api/slack/oauth/install/route.ts, app/api/slack/oauth/callback/route.ts.
The local API fails closed without its launch token. With no token file on disk, Weft's server used to skip every token check; now reads without the board's header and every elevated write are refused. First launch is unaffected (the desktop app writes the token before it starts the server); pnpm dev in a plain browser opts out with WEFT_NO_TOKEN=1. lib/guard.ts, package.json.
The onboarding-stats collector is rate-limited. weft.zip's /api/telemetry/collect now takes 60 posts a minute per address (429 with retry-after) and refuses browser posts from other sites, so a script can't flood the stats. lib/telemetry-limit.ts, lib/fixed-window.ts.
The free Puku trial has a daily ceiling across every account. Weft accounts are free, so the per-account cap alone could be stretched by opening more; now the whole trial stops for the day at about a hundred accounts' worth (15,000 requests, 100M tokens; PUKU_TRIAL_GLOBAL_DAILY_* override it), with a clear 429 message. Pro workspaces aren't counted. lib/puku-proxy.ts, supabase/migrations/20261026151000_puku_trial_global_usage.sql.