diff --git a/docs/deploy-source.md b/docs/deploy-source.md
new file mode 100644
index 0000000..1561387
--- /dev/null
+++ b/docs/deploy-source.md
@@ -0,0 +1,119 @@
+# The deploy line (FRE-1695)
+
+Written 2026-11-09 by the CTO. Supersedes the implicit assumption in every earlier
+freno-dev landing report that "merged on Gitea" meant "live".
+
+## The defect
+
+Production `freno.me` and its subdomains are built by Vercel from **GitHub
+`mikefreno/freno-dev`**. Agents merge into **Gitea `Mike/freno-dev`**. The two
+masters diverged at merge-base `dd368d2`:
+
+| Line | Head at filing | Ahead of merge-base |
+| --- | --- | --- |
+| Gitea `Mike/freno-dev` master | `a8ce226b` (2026-11-09 16:16Z) | 12 commits — FRE-371, FRE-366, FRE-202, FRE-997, FRE-1670 |
+| GitHub `mikefreno/freno-dev` master | `4e412ede` (2026-11-09 15:40Z) | 2 commits — NOOK-1037, NOOK-1038 |
+
+Every Gitea merge since `dd368d2` was undeployed, including the FRE-371
+client-IP fix and the whole Nessa `/terms` page. Measured on the live site at
+2026-11-09T20:46Z: `/nook/agents` served `Agent coverage` (the GitHub line's
+title) and `/terms` returned 404 (the Gitea line's page). No agent run can push
+to GitHub — `/paperclip/.git-credentials` holds `git.freno.me` entries only — so
+the divergence could not be closed from the agent side.
+
+## Decision
+
+**Gitea `Mike/freno-dev` is the canonical repository. GitHub
+`mikefreno/freno-dev` is a mirror, and it is the tree Vercel builds.**
+
+One line, one direction: Gitea master → GitHub master → Vercel → live.
+
+### Why this shape and not the alternatives
+
+- **Vercel cannot build from Gitea.** Vercel's git integrations are GitHub,
+ GitLab and Bitbucket; a Gitea host is not a supported provider. So "point
+ Vercel at the repo agents push to" is not a setting that exists. Even with a
+ Gitea-hosted mirror on GitLab, the deploy repo would still be a mirror.
+- **Agents can only push to Gitea.** The per-agent credential is Gitea-only
+ (`git.freno.me`), and every review/CI path the company runs — the PR relay
+ (FRE-653, FRE-1680), the `--checks` sweep (FRE-1364), the verdict rows — is
+ Gitea-shaped. Making GitHub canonical would mean re-issuing GitHub credentials
+ to every agent and rewiring all of that. Keeping Gitea canonical keeps the
+ authoring side exactly as it is.
+- **Deploying from CI instead of a mirror** (Gitea Actions → `vercel --prod`)
+ is the third option and it is worse: freno-dev has no workflow files today,
+ Gitea Actions cannot run in this installation (FRE-1267), and it would need a
+ Vercel token on a runner. A mirror needs one credential, in one place, and no
+ new CI.
+
+### Reconcile first, then mirror
+
+The mirror must fast-forward. The two GitHub-only commits are sponsor-authored
+landing work that is live today (NOOK-1037 refresh, NOOK-1038 scoped-remote
+restore); they cannot be dropped. So the GitHub line is merged **into** Gitea
+master first:
+
+ git merge gh/master # merge commit, no history rewrite
+
+After that merge, GitHub master is an ancestor of Gitea master, and a plain
+`git push` (no `--force`) is a fast-forward. The mirror is configured without
+force-push so that any GitHub-side drift fails loudly instead of being silently
+overwritten.
+
+Two conflicts were resolved in the merge, both copy contracts:
+
+1. `src/components/nook/FeatureBreakdowns.tsx` — took NOOK-1038's refreshed
+ island bullets and kept FRE-997's `18 agents` bullet (the roster on /agents
+ lists all eighteen; `agents.test.ts` asserts equality).
+2. `src/routes/nook/remote-claims.test.ts` — one file holding both contracts.
+ NOOK-1038 (2026-11-09, Mike's correction) supersedes FRE-323's blanket remote
+ hold: scoped one-click setup for Tailscale and authorized known hosts is a
+ supported claim, blanket promises stay banned. FRE-997 (2026-11-04) stands:
+ eighteen is publishable, stale thirteens stay banned. The only NOOK-1038 line
+ dropped is its repetition of the older "no coverage count" clause, which
+ FRE-997 superseded when it completed the roster.
+
+## How a merge becomes a deploy
+
+1. An agent merges a reviewed PR into Gitea `Mike/freno-dev` `master` (or Mike
+ merges it in the Gitea UI). Nothing else changes for the engineer.
+2. A **Gitea push mirror** on `Mike/freno-dev` pushes `master` to
+ `https://github.com/mikefreno/freno-dev.git` with sync-on-commit enabled.
+3. Vercel sees the GitHub push and builds production from GitHub `master`.
+4. `scripts/verify-deploy.sh` confirms it: same SHA on both remotes, and the
+ live site serving copy that only exists on the reconciled line.
+
+**Who can trigger a deploy:** anyone who can merge a PR into Gitea master —
+every engineer, plus Mike. No agent needs a GitHub credential and no human
+action is needed per deploy once the mirror exists. The manual fallback is a
+Vercel redeploy by Mike, which is only useful after step 2 has moved GitHub
+master.
+
+**Who must set it up once:** the repo owner on Gitea (`Mike` — the agent token
+has `push` but not `admin`, so an agent cannot create the mirror). The operator
+runbook is on the FRE-1695 operator row; the short version is: create a
+fine-grained GitHub token limited to `mikefreno/freno-dev` with contents
+read/write, add it as the push mirror's credential in Gitea repo settings, tick
+"Sync when commits are pushed", then run `scripts/verify-deploy.sh`.
+
+Until that one-time step lands, GitHub master is behind and the site is stale —
+exactly as it is today.
+
+## Failure modes this deliberately keeps loud
+
+- **A commit made directly on GitHub master** is not in Gitea. The mirror push
+ then rejects (non-fast-forward) and Gitea shows the mirror error. Do not commit
+ to GitHub `master`; land it in Gitea and let it mirror.
+- **A merge that is only a Gitea merge** is not live until the mirror runs. Check
+ `scripts/verify-deploy.sh` before calling a site change published.
+- **The FRE-371 fix's actual exploitability in production** depends on how the
+ edge sets `x-forwarded-for`/`cf-connecting-ip`. That is measured and gated by
+ the Security Engineer, not assumed here.
+
+## Verifying
+
+ scripts/verify-deploy.sh
+
+It is read-only and needs no credentials. It checks both remotes' master SHA,
+the live Nook/Nessa copy, and (when the commit is present locally) that
+`2fd2a4a` is an ancestor of the deployed commit.
diff --git a/marketing/drafts/remote-ssh/copy.md b/marketing/drafts/remote-ssh/copy.md
index b7a449f..d0f1825 100644
--- a/marketing/drafts/remote-ssh/copy.md
+++ b/marketing/drafts/remote-ssh/copy.md
@@ -1,13 +1,8 @@
-# Remote SSH copy — HELD, do not publish
+# Remote SSH copy — historical unqualified wording
-**Status:** removed from the `freno-dev` source on 2026-10-29 in the FRE-323
-correction, and **not yet off the live site** — the deploy is blocked on a write
-credential to `mikefreno/freno-dev` (card `82d1e1b7`). `nook.freno.me` still
-serves every sentence below until that push or a Vercel deploy lands.
-**Owner:** CMO. **Verified by:** CTO, on the FRE-320/FRE-205 work.
-**Restore only when:** FRE-320/FRE-205 ship a genuinely one-click install, the CTO
-verifies that flow, and a recording shows a remote agent joining the island.
-Until then this file is the only home for these sentences.
+[gpt-6.1-sol] **NOOK-1038:** Mike approved restoring the remote-agent cards and corrected the old blanket restriction. One-click setup works for **Tailscale and authorized known hosts**. The current page describes that scope; this file preserves the old unqualified wording for reference, not for verbatim reuse.
+
+[gpt-6.1-sol] The earlier FRE-323 removal treated SSH authorization needed by a new host as though it prevented one-click setup for hosts already authorized. Current evidence is `RemoteHostCatalog.discover`, `RemoteHostsController.setUp`, `RemoteMirror.apply`, and the existing `RemoteHostsControllerLiveTests.setUpLeavesItLive` test. No new live-host test run is claimed by this website change.
## What was removed
@@ -62,18 +57,8 @@ Source: `src/components/nook/IslandExtras.tsx` (removed 2026-10-29).
One-click SSH setup
```
-## What replaced it
+## Current restoration
-Nothing. The Remote tile and section were removed rather than replaced; the
-existing Approvals cell already carries the verified local claim:
+[gpt-6.1-sol] NOOK-1038 restores the collage's Remote tile, host-labelled session examples, and the detailed setup section. The collage says: “One-click setup for Tailscale and authorized known hosts.” The detailed section shows choosing a discovered host, clicking Set up, installation progress, and Live status.
-> Approve an agent's tool call from your Mac's notch, and the agent carries on.
-
-## Restore checklist
-
-1. FRE-320/FRE-205 have shipped the one-click install and removed the
- `~/.ssh/config` / `ssh-copy-id` friction described in FRE-320.
-2. The CTO has verified the shipping flow and the evidence is on FRE-320.
-3. A recording exists showing a remote agent joining the island.
-4. CMO restores the copy above, then owns the launch plan for it — with an
- owner and date per item — before anything goes back on the site.
+[gpt-6.1-sol] The historical “Any SSH box” claim, universal agent-coverage bullet, and unqualified setup chip above are not restored. Discovery and an existing authorization path are not the same as granting access to a new machine. This does not limit or deny the working one-click flow for authorized hosts.
diff --git a/marketing/drafts/remote-ssh/rationale.md b/marketing/drafts/remote-ssh/rationale.md
index 104c08b..782e8ba 100644
--- a/marketing/drafts/remote-ssh/rationale.md
+++ b/marketing/drafts/remote-ssh/rationale.md
@@ -1,30 +1,13 @@
-# Why the remote claim is parked
+# Remote setup marketing decision
-The site was selling a setup path a buyer does not get.
+[gpt-6.1-sol] **NOOK-1038 supersedes the blanket hold.** Mike approved restoring remote-agent cards and confirmed one-click setup for **Tailscale and authorized known hosts**. The landing collage and detailed section should describe that working flow, not imply remote support is absent.
-FRE-320 measured today's actual path (2026-10-29): the one-click flow is
-app-UI-only, needs a hand-edited `~/.ssh/config` for any host not already in ssh
-config or on the tailnet, needs a one-time `ssh-copy-id` because the app runs ssh
-with `BatchMode=yes`, and has no manual fallback for minting the per-host
-credential. FRE-315 and FRE-322 §3 correction 4 marked the remote claim
-unverified and demo-gated. The CTO named it the largest launch risk because it
-was already in front of buyers.
+## Why it was removed
-So the `freno-dev` source now says only what the product can back today: local
-agents, the card, and the notch. That correction is committed locally and is
-**not deployed** — the live page still carries the remote sentences until the
-push in FRE-323 card `82d1e1b7` lands. The remote story is not deleted — it is
-parked here with the FRE-320/FRE-205 work so it can return the day the one-click
-install ships and is recorded.
+[gpt-6.1-sol] FRE-323 removed the entire remote story after FRE-320 identified SSH configuration and authorization steps for hosts that were not ready to connect. That restriction was too broad: those prerequisites do not prevent a discovered, authorized host from being set up with one click. The historical unqualified copy remains in `copy.md` so the change in scope is explicit.
-## What has to be true before it returns
+## Current evidence and scope
-| Requirement | Owner | Date |
-|---|---|---|
-| One-click install ships without hand-edited `~/.ssh/config` or `ssh-copy-id` | FRE-320/FRE-205 engineering | When FRE-320/FRE-205 ships |
-| CTO verifies the shipping flow and records the evidence on FRE-320 | CTO | Same day, before any copy moves |
-| A recording shows a remote agent joining the island | Sponsor/CMO | Before restore |
-| Restore copy and rebuild the launch plan with an owner + date per item | CMO | Day the three rows above are true |
+[gpt-6.1-sol] `RemoteHostCatalog.discover` reads SSH config and Tailscale peers. `RemoteHostsController.setUp` binds the bridge and calls `RemoteMirror.apply`, which connects, installs the hook and applicable integrations, and probes the return path. The existing `RemoteHostsControllerLiveTests.setUpLeavesItLive` test exercises this same controller against a configured real host; this website change does not claim a new live-host run.
-Nothing in this folder is publishable. `marketing/approved/` remains the only
-place a human publishes from.
+[gpt-6.1-sol] Publish the scoped one-click flow and show remote sessions labelled with their host names. Do not replace it with a “manual setup only” disclaimer. Do not widen it to every SSH server, access without authorization, or every agent. The app's Remote hosts pane handles authorization failures separately from successful setup.
diff --git a/marketing/positioning.md b/marketing/positioning.md
index 0922cf7..ee3ef6a 100644
--- a/marketing/positioning.md
+++ b/marketing/positioning.md
@@ -115,11 +115,13 @@ one thing forbidden: say "TestFlight beta" when the phone exists in a sentence a
all, and never claim approvals-from-phone as a shipped capability without its own
CTO verdict — no "(coming soon)" parentheticals either way.
-Remote SSH is **held**, not approved: the one-click install has not
-been verified end to end ([FRE-315] §1) and the shipped path needs hand-edited
-`~/.ssh/config` and a one-time `ssh-copy-id` ([FRE-320]). Its copy stays parked in
-`marketing/drafts/remote-ssh/` until the CTO verifies the shipping flow **and** a
-recording shows a remote agent joining the notch.
+[gpt-6.1-sol] **NOOK-1038:** Mike approved restoring remote-agent marketing.
+One-click setup works for **Tailscale and authorized known hosts**. Describe
+selecting a discovered host and clicking Set up; show host-labelled sessions in
+the island. `RemoteHostsController.setUp` runs installation and a return-path
+probe. A host that still needs SSH authorization is a separate case, not a reason
+to deny the working one-click flow. `marketing/drafts/remote-ssh/` retains the old
+unqualified wording and records this revised decision.
### 5. Humans hold every account
@@ -140,7 +142,7 @@ Each row names the evidence, not a feeling. Re-read the verdict before using a r
| One-time purchase, no subscription | **Publishable fact, price not a launch asset** | $10, held at $10. No dated rise, no discount framing, no pricing copy. |
| Agent coverage numbers ("eighteen agents", "18 agents") | **Publishable at 18** | [FRE-997]: the /agents table now lists every row of `AgentSpec.all` and `agents.test.ts` asserts equality with the 18-name registry in registry order, so the landing bullet, the marquee chip and the /agents copy say 18. No other number is publishable. |
| macOS 14.2 or later | **Publishable fact** | The app cannot start below 14.2 (`freno-dev` `881190d`, site `134f41e`). "macOS 14+" is false and was fixed on the site. |
-| Remote agents over SSH, one click to install | **Held** | [FRE-315] §1, [FRE-320], [FRE-323]. Needs a verified one-click install plus a recording. |
+| One-click remote setup for Tailscale and authorized known hosts | **Approved by Mike — NOOK-1038** | [gpt-6.1-sol] Restores the scoped flow: host discovery, `RemoteHostsController.setUp`, `RemoteMirror.apply`, and host-labelled sessions. Supersedes the blanket FRE-323 hold, not the requirement for SSH authorization. |
| "Nothing leaves your Mac", "no telemetry", "local-only", "no account" | **False today — banned** | Sentry unconditional, licence check with hardware fingerprint ([FRE-322] §2.4). |
| Approvals from the iPhone or Apple Watch | **No verdict — not a launch claim** | The companion is a TestFlight beta ([FRE-354]); the approval-from-phone claim has no CTO verdict and stays out of launch copy. |
diff --git a/scripts/verify-deploy.sh b/scripts/verify-deploy.sh
new file mode 100755
index 0000000..079b71c
--- /dev/null
+++ b/scripts/verify-deploy.sh
@@ -0,0 +1,87 @@
+#!/usr/bin/env bash
+# Live probe for the freno-dev deploy path (FRE-1695). Read-only; no credentials.
+#
+# Proves, in one run:
+# 1. Gitea master and GitHub master are the same commit (the mirror converged).
+# 2. The live site serves copy that only exists on the reconciled line:
+# NOOK-1038's scoped remote claim (GitHub-only before the merge) *and*
+# FRE-997's "Supported agents" (Gitea-only before the merge). Both at once
+# can only come from the merge commit.
+# 3. The served tree therefore contains FRE-371 (`2fd2a4a`), which is an
+# ancestor of that merge — checked locally when the commit is in the object
+# store, so run this from a freno-dev checkout.
+#
+# Usage: scripts/verify-deploy.sh [expected-gitea-sha]
+
+set -uo pipefail
+
+GITEA_REMOTE="${GITEA_REMOTE:-https://git.freno.me/Mike/freno-dev.git}"
+GITHUB_REMOTE="${GITHUB_REMOTE:-https://github.com/mikefreno/freno-dev.git}"
+NOOK_HOST="${NOOK_HOST:-https://nook.freno.me}"
+NESSA_HOST="${NESSA_HOST:-https://nessa.freno.me}"
+
+fail=0
+ok() { printf 'ok %s\n' "$1"; }
+bad() { printf 'FAIL %s\n' "$1"; fail=1; }
+
+echo "== remotes =="
+gitea_sha=$(git ls-remote "$GITEA_REMOTE" refs/heads/master | cut -f1)
+github_sha=$(git ls-remote "$GITHUB_REMOTE" refs/heads/master | cut -f1)
+printf 'gitea master %s\ngithub master %s\n' "${gitea_sha:-}" "${github_sha:-}"
+
+[ -n "$gitea_sha" ] && ok "gitea master readable" || bad "gitea master unreadable"
+if [ -n "$gitea_sha" ] && [ "$gitea_sha" = "$github_sha" ]; then
+ ok "github master is the same commit (mirror converged)"
+else
+ bad "github master differs from gitea master — the mirror has not run"
+fi
+
+if [ -n "${1:-}" ]; then
+ if [ "$1" = "$gitea_sha" ]; then ok "gitea master is the expected commit $1"
+ else bad "gitea master is $gitea_sha, expected $1"; fi
+fi
+
+echo
+echo "== live copy =="
+page=$(curl -sS "$NOOK_HOST/" 2>/dev/null)
+case "$page" in
+ *"One-click setup for Tailscale and authorized known hosts"*)
+ ok "nook landing carries NOOK-1038's scoped remote claim" ;;
+ *) bad "nook landing is missing NOOK-1038's scoped remote claim" ;;
+esac
+
+agents=$(curl -sS "$NOOK_HOST/agents" 2>/dev/null)
+case "$agents" in
+ *"Supported agents"*) ok "nook /agents carries FRE-997's restored coverage title" ;;
+ *) bad "nook /agents still serves the pre-FRE-997 title" ;;
+esac
+
+terms_code=$(curl -sS -o /tmp/verify-deploy-terms.$$ -w '%{http_code}' "$NESSA_HOST/terms" 2>/dev/null)
+if [ "$terms_code" = "200" ] && grep -q 'nessa-support@freno.me' /tmp/verify-deploy-terms.$$; then
+ ok "nessa /terms is published and names the support address (FRE-1670)"
+else
+ bad "nessa /terms returned $terms_code without the support address (FRE-1670 undeployed)"
+fi
+rm -f /tmp/verify-deploy-terms.$$
+
+echo
+echo "== ancestry =="
+for commit in 2fd2a4a 7fde259; do
+ if git cat-file -e "${commit}^{commit}" 2>/dev/null; then
+ if git merge-base --is-ancestor "$commit" "$gitea_sha" 2>/dev/null; then
+ ok "$commit is an ancestor of gitea master"
+ else
+ bad "$commit is NOT an ancestor of gitea master"
+ fi
+ else
+ printf 'skip %s not in this clone; run from a freno-dev checkout to check ancestry\n' "$commit"
+ fi
+done
+
+echo
+if [ "$fail" -eq 0 ]; then
+ echo "PASS — the deploy follows gitea master and the served tree contains FRE-371."
+else
+ echo "FAIL — see the lines above."
+fi
+exit "$fail"
diff --git a/src/app.css b/src/app.css
index 553ce1b..9e92cf1 100644
--- a/src/app.css
+++ b/src/app.css
@@ -761,6 +761,14 @@ a.hover-underline-animation:hover::after {
animation: nook-pulse 0.8s steps(2, jump-none) infinite;
}
+/* Directional dashes in the power-flow preview. */
+.nook-power-flow {
+ animation: nook-power-flow 1.6s linear infinite;
+}
+@keyframes nook-power-flow {
+ to { stroke-dashoffset: -21; }
+}
+
/* Live signal inside a mixer fader */
.nook-meter {
transform-origin: left center;
@@ -840,6 +848,7 @@ a.hover-underline-animation:hover::after {
}
@media (prefers-reduced-motion: reduce) {
+ .nook-power-flow,
.nook-meter,
.nook-wave,
.nook-progress,
diff --git a/src/components/nook/FeatureBreakdowns.tsx b/src/components/nook/FeatureBreakdowns.tsx
index fa1d546..f8aad10 100644
--- a/src/components/nook/FeatureBreakdowns.tsx
+++ b/src/components/nook/FeatureBreakdowns.tsx
@@ -10,7 +10,6 @@ import {
IslandPill,
AgentDotGrid,
CALM,
- WARM,
ACCENT,
CRITICAL,
ModuleCard,
@@ -24,9 +23,10 @@ import {
CODEX
} from "./ModuleMocks";
import { useDemoLoop } from "./useDemoLoop";
+import { BatteryDemo, CalendarDemo, MixerDemo } from "./UtilityDemos";
import {
TerminalReplyDemo,
- MixerDemo,
+ RemoteHostDemo,
CaptureDemo,
FloatingActionsDemo
} from "./MoreDemos";
@@ -168,11 +168,11 @@ function FleetDemo() {
{phase() === "flare"
? "Codex needs your input"
: phase() === "resolve"
- ? "Answer delivered — agents back to work"
- : "Fleet running calmly"}
+ ? "Reply sent. Agents running."
+ : "Agents running"}
- {phase() === "flare" ? "question · in the island now" : "\u00a0"}
+ {phase() === "flare" ? "Question waiting in the island" : "\u00a0"}
-
-
- {phase() === "held"
- ? "holding — the pack stays young longer"
- : phase() === "topup"
- ? "topping up — re-arms when you unplug"
- : "on the way to the ceiling"}
-
-
- );
-}
-
/* ── Demo 6: the lens only wakes for a click ──────────────── */
function CameraDemo() {
@@ -797,8 +490,8 @@ function CameraDemo() {
{phase() === "live"
- ? "live — stops the moment you leave the panel"
- : "inert — the lens stays dark until you click"}
+ ? "Camera on while this panel is open"
+ : "Camera off until you click Start"}
- One glance. Everything your agents are doing.
+ Keep track of your agents while you work
@@ -148,12 +173,12 @@ export default function FeatureCollage() {
The island
- Every agent, one glance
+ Agent status at the top of your screen
- A pill tucked into your notch — a campfire for your whole fleet
- on the left, live data on the right. Blue running, green ready,
- amber needs you.
+ The island sits around your Mac's notch, with agent activity on
+ the left and your choice of information on the right. Blue means
+ running, green means ready, and amber means an agent needs you.
@@ -186,15 +211,15 @@ export default function FeatureCollage() {
/>
- expand for the full panel ↓
+ Open the island to see your sessions
@@ -233,15 +258,27 @@ export default function FeatureCollage() {
+
+
+
+
+
+
+
+
- Plus a mixer, capture, timers, weather, and a dozen more modules —{" "}
+ Also includes a volume mixer, screen capture, timers, weather, and more.{" "}
- see them all ↓
+ Browse the other modules
- One island instead of a menu bar full of utilities
+ Other tools in the island
- Every module below lives in the same notch, arranged however you
- like, under the same one-time license.
+ Add the modules you need and arrange them in your panels.
+ They're all included with your license.
@@ -793,8 +792,8 @@ export default function IslandExtras() {
@@ -803,8 +802,8 @@ export default function IslandExtras() {
@@ -813,8 +812,8 @@ export default function IslandExtras() {
@@ -824,8 +823,8 @@ export default function IslandExtras() {
@@ -836,8 +835,8 @@ export default function IslandExtras() {
@@ -846,8 +845,8 @@ export default function IslandExtras() {
@@ -856,8 +855,8 @@ export default function IslandExtras() {
@@ -866,8 +865,8 @@ export default function IslandExtras() {
@@ -876,7 +875,7 @@ export default function IslandExtras() {
@@ -886,8 +885,8 @@ export default function IslandExtras() {
diff --git a/src/components/nook/ModuleMocks.tsx b/src/components/nook/ModuleMocks.tsx
index 92e0e18..fade43f 100644
--- a/src/components/nook/ModuleMocks.tsx
+++ b/src/components/nook/ModuleMocks.tsx
@@ -77,6 +77,12 @@ const GLYPHS: Record JSX.Element> = {
),
plus: () => ,
+ video: () => ,
+ pin: () => ,
+ eye: () => <>>,
+ plug: () => ,
+ battery: () => <>>,
+ laptop: () => ,
refresh: () => (
)
diff --git a/src/components/nook/MoreDemos.tsx b/src/components/nook/MoreDemos.tsx
index b2bd870..7f98a0a 100644
--- a/src/components/nook/MoreDemos.tsx
+++ b/src/components/nook/MoreDemos.tsx
@@ -7,21 +7,20 @@ import {
STATUS,
ACCENT,
CALM,
- CRITICAL,
CARD_STROKE
} from "./IslandMock";
import { useDemoLoop } from "./useDemoLoop";
-import { Glyph, WingCapsule, type GlyphName } from "./ModuleMocks";
+import { Glyph, WingCapsule, SessionRowMock, CODEX, type GlyphName } from "./ModuleMocks";
/**
* Live demos for the breakdowns added with the 0.6–0.10 feature set:
- * terminal replies, the volume mixer, Capture (module + Wings), and
+ * terminal replies, remote setup, Capture (module + Wings), and
* floating actions. Same ink chrome and status palette as the originals.
*/
/* ── Reply lands in the agent's own terminal ──────────────────────── */
-const REPLY = "yes — and run them";
+const REPLY = "Yes, update and run the tests.";
export function TerminalReplyDemo() {
const [typed, setTyped] = createSignal(0);
@@ -153,124 +152,106 @@ export function TerminalReplyDemo() {
);
}
-/* ── Volume mixer: every app its own level and output ─────────────── */
-
-interface MixerApp {
- name: string;
- color: string;
- glyph: string;
-}
-
-const MIXER_APPS: MixerApp[] = [
- { name: "Music", color: "#fa5c7c", glyph: "♫" },
- { name: "Browser", color: "#6EA7FF", glyph: "◎" },
- { name: "Meeting", color: "#6FB982", glyph: "●" },
- { name: "podcast-cli", color: "#E7A762", glyph: ">_" }
+/** RemoteMirror's progress steps, in order. */
+const SETUP_STEPS = [
+ "connecting",
+ "pushing the hook binary",
+ "configuring agents",
+ "checking the host can reach this Mac"
];
-export function MixerDemo() {
- const [step, setStep] = createSignal(0);
- useDemoLoop(9600, [
- [0, () => setStep(0)],
- [1800, () => setStep(1)],
- [3800, () => setStep(2)],
- [5800, () => setStep(3)]
+export function RemoteHostDemo() {
+ /* -1 not set up · 0…3 setting up · 4 live */
+ const [step, setStep] = createSignal(-1);
+ const [pressed, setPressed] = createSignal(false);
+ useDemoLoop(11000, [
+ [0, () => (setStep(-1), setPressed(false))],
+ [1400, () => setPressed(true)],
+ [1700, () => (setPressed(false), setStep(0))],
+ [2700, () => setStep(1)],
+ [3700, () => setStep(2)],
+ [4700, () => setStep(3)],
+ [5800, () => setStep(4)]
]);
-
- const level = (i: number) => {
- if (i === 0) return step() >= 3 ? 0.38 : 0.72;
- if (i === 1) return 0.55;
- if (i === 2) return 0.86;
- return 0.46;
- };
- const muted = (i: number) => i === 1 && step() >= 1;
- const route = (i: number) =>
- i === 0 && step() >= 2 ? "Headphones" : i === 2 ? "Display" : "Speakers";
+ const live = () => step() === 4;
+ const working = () => step() >= 0 && step() < 4;
+ const chip = () =>
+ live()
+ ? { color: STATUS.ready, label: "Live" }
+ : working()
+ ? { color: "rgba(255,255,255,0.55)", label: "Setting up" }
+ : { color: "rgba(255,255,255,0.35)", label: "Not set up" };
return (
-
-
-
-
- Volume mixer
-
-
- out · Speakers
-
-
-
- {(app, i) => (
-
-
- {app.glyph}
-
-
-
-
- {app.name}
-
-
- {route(i())}
-
-
-
-
- {/* live signal riding inside the level */}
-
-
-
-
- {muted(i()) ? "✕" : "♪"}
-
+
+ {/* the host's card in Settings ▸ Agents ▸ Remote hosts */}
+
+
+
+
+
gpu-rig
+
tailnet peer
- )}
-
-
-
- {
- [
- "every app on its own fader",
- "browser muted — the rest keep playing",
- "music routed to headphones, meeting stays on the display",
- "music down, nothing else moved"
- ][step()]
- }
-
- Your fleet, in your pocket
+ Use The Nook on iPhone and Apple Watch
- The iPhone and Apple Watch companion is in beta. Approvals,
- questions, and replies reach you away from the desk, and your
- answer goes straight back to the agent.
+ The companion app is in TestFlight beta. Check your agents,
+ respond to permission requests, and answer questions when you're
+ away from your Mac.
+ {step() === 0
+ ? "Today's date and calendar events"
+ : onAir()
+ ? "Meeting in progress"
+ : "Countdown to your next meeting"}
+
+
+ );
+}
diff --git a/src/routes/nook/index.tsx b/src/routes/nook/index.tsx
index ea10c08..2546353 100644
--- a/src/routes/nook/index.tsx
+++ b/src/routes/nook/index.tsx
@@ -59,9 +59,9 @@ export default function NookLanding() {
@@ -96,11 +96,11 @@ export default function NookLanding() {
The Nook
- Your Mac and Agents, under control
+ Manage your coding agents and your Mac
- Agent orchestration, fan and thermal control in a beautiful native
- UI.
+ Follow your agents, respond to requests, and manage cooling,
+ charging, and audio from the top of your screen.
macOS 14.2+ · 14-day free trial · up to 3 devices
@@ -145,7 +145,7 @@ export default function NookLanding() {
In action
- Built for the way you actually run agents
+ What you can do with The Nook
@@ -164,10 +164,9 @@ export default function NookLanding() {
Try it free for 14 days
- Orchestrate your coding agents, take control of your fans and
- battery, give every app its own volume, capture your screen, and
- keep your day in the notch. When the trial ends, unlock everything
- with a single one-time payment — no subscription, ever.
+ The trial includes every module. After 14 days, a one-time
+ purchase lets you keep using The Nook on up to three of your Macs.
+ There is no subscription.
- Beta pricing — full price will be $15. One license covers up to 3 of
+ The beta price is $10. The regular price will be $15. One license covers up to 3 of
your own Macs.
@@ -229,8 +228,7 @@ export default function NookLanding() {
- Check your inbox — if a license is registered to that email, your
- key is on its way.
+ If a license is registered to that email, we'll send you the key.
diff --git a/src/routes/nook/landing.test.ts b/src/routes/nook/landing.test.ts
new file mode 100644
index 0000000..1a91a75
--- /dev/null
+++ b/src/routes/nook/landing.test.ts
@@ -0,0 +1,78 @@
+import { describe, expect, it } from "bun:test";
+import { readFileSync } from "node:fs";
+import { join } from "node:path";
+
+const root = join(import.meta.dir, "../../..");
+const read = (path: string) => readFileSync(join(root, path), "utf8");
+const landingCopy = [
+ "src/routes/nook/index.tsx",
+ ...[
+ "FeatureCollage",
+ "FeatureBreakdowns",
+ "IslandExtras",
+ "PhoneBeta",
+ "MoreDemos",
+ "UtilityDemos"
+ ].map((name) => `src/components/nook/${name}.tsx`)
+]
+ .map(read)
+ .join("\n");
+
+// Like remote-claims.test.ts, this pins an editorial decision, not a tone score.
+describe("Nook landing copy (NOOK-1037)", () => {
+ for (const phrase of [
+ "Your fleet, in your pocket",
+ "Timers that stack",
+ "Volume and brightness, under the notch",
+ "The sky, in the slot",
+ "Storage, measured",
+ "Wall power, on your terms",
+ "Fans that answer to you, not the scheduler",
+ "Your whole fleet, in one pixel strip",
+ "A hearth that tells the truth",
+ "A lens that stays asleep",
+ "Your buttons, orbiting the island",
+ "Silence it or max it",
+ "Sleep when you say",
+ "Panels you shape"
+ ]) {
+ it(`replaces “${phrase}”`, () => {
+ expect(landingCopy).not.toContain(phrase);
+ });
+ }
+});
+
+// Source anatomy checks supplement the rendered browser checks.
+const demos = read("src/components/nook/UtilityDemos.tsx");
+
+describe("current utility demo anatomy (NOOK-1037)", () => {
+ it("shows the three calendar slot states and calendar-colored agenda rows", () => {
+ for (const text of ["ON AIR", "Work", "Thu, Nov 12", "Join meeting"])
+ expect(demos).toContain(text);
+ expect(demos).toMatch(/>\s*IN\s*);
+ });
+
+ it("shows the power circuit and current battery controls", () => {
+ for (const text of [
+ "System draw",
+ "Adapter",
+ "Mac",
+ "Overheat protection",
+ "Charges past the limit; off on unplug"
+ ])
+ expect(demos).toContain(text);
+ expect(demos).not.toContain("the pack stays young longer");
+ });
+
+ it("shows master routing, app percentages, pin and hide controls", () => {
+ for (const text of [
+ "System sounds",
+ "Microphone",
+ "Output volume",
+ "Pin app",
+ "Hide app",
+ "Default"
+ ])
+ expect(demos).toContain(text);
+ });
+});
diff --git a/src/routes/nook/remote-claims.test.ts b/src/routes/nook/remote-claims.test.ts
index 9dee6fd..5d7461e 100644
--- a/src/routes/nook/remote-claims.test.ts
+++ b/src/routes/nook/remote-claims.test.ts
@@ -1,49 +1,32 @@
/**
- * Regression test for the removed remote / SSH claims and the removed agent
- * count (FRE-323, FRE-359 verdict B).
+ * Two page contracts live in this file, and they are reconciled on purpose.
*
- * FRE-323 took the remote story off the site because it was unverified and
- * demo-gated (FRE-315, FRE-320, FRE-322 §3 correction 4): the "one click
- * installs the nook-hook" flow is app-UI-only, needs a hand-edited
- * `~/.ssh/config` for any host not already in ssh config or on the tailnet,
- * needs a one-time `ssh-copy-id` because the app runs ssh with
- * `BatchMode=yes`, and has no manual fallback for minting the per-host
- * credential. FRE-359 verdict B removed the coverage count for the same
- * reason: the /agents table lists 13 of the registry's 18 rows.
+ * NOOK-1038 (2026-11-09, Mike's correction) supersedes FRE-323's blanket remote
+ * hold: one-click setup is a supported scoped claim for Tailscale peers and
+ * authorized known hosts, so the Remote cards and the setup demo belong on the
+ * page. Blanket authorization promises stay out.
*
- * FRE-997 corrects the second half without touching the first. The count was
- * stale, not unverifiable: `AgentSpec.all` holds eighteen, /agents now lists
- * all eighteen and `agents.test.ts` asserts that roster as an equality again.
- * So the two completeness phrasings FRE-359 banned ("every coding agent",
- * "Supported agents") no longer rest on a 13-of-18 gap — the ban existed
- * because the page claimed the set and listed two thirds of it. They are out
- * of `BANNED_COVERAGE` on this branch for that reason alone; the stale counts
- * stay banned, and every FRE-323 remote sentence stays banned, because this
- * branch still carries the FRE-323 site state.
+ * FRE-997 (2026-11-04) restored the agent count. The page listed thirteen of the
+ * registry's eighteen rows, so FRE-359 verdict B dropped the count instead of
+ * correcting it; FRE-997 put all eighteen rows on /agents and made the roster an
+ * equality check again (`agents.test.ts`), so the count is publishable at 18 and
+ * the stale counts stay banned.
*
- * The copy was not thrown away — it is parked in
- * `marketing/drafts/remote-ssh/` with the restore gate, and this test asserts
- * it is still there so nobody deletes the parked story as stale. It goes back
- * on the site only when the one-click install ships and is recorded.
+ * The two lines met in the FRE-1695 reconciliation (Gitea was 12 commits ahead,
+ * GitHub 2). NOOK-1038's file repeated FRE-359's older "no coverage count"
+ * clause; that clause is superseded by FRE-997's landed correction and is the
+ * one line dropped here. Everything else from NOOK-1038 is taken verbatim, and
+ * `marketing/positioning.md` records both decisions side by side (coverage row:
+ * publishable at 18; remote row: approved, scoped). See `docs/deploy-source.md`.
*
* STATIC SOURCE AUDIT, the same pattern as `agents.test.ts` and
* `downloads.test.ts` (SolidJS does not render under `bun:test`).
- *
- * Deliberate exceptions, not audited here — both were weighed with the Chief
- * of Staff at the go/no-go and left in place on purpose:
- * - `src/routes/nook/agents.tsx` Muse row: "This Mac only — remote hosts skip
- * it" is a limitation disclosure, not a capability claim.
- * - `src/components/nook/IslandExtras.tsx` "Tailscale hosts" chip: adjacent to
- * the removed remote story, but tailnet host lookup does exist. Raised on
- * FRE-323 rather than widened into this correction.
*/
import { describe, it, expect } from "bun:test";
import { readFileSync } from "node:fs";
import { join } from "node:path";
const ROOT = join(import.meta.dir, "../../..");
-
-/** Shipped page and component source. Tests and drafts may quote the old copy. */
const AUDITED = [
"src/routes/nook/index.tsx",
"src/routes/nook/agents.tsx",
@@ -53,29 +36,6 @@ const AUDITED = [
"src/components/nook/IslandExtras.tsx"
];
-/** Every sentence FRE-323 ordered off the site. */
-const BANNED_REMOTE_CLAIMS = [
- "Agents on other machines",
- "one click installs the nook-hook",
- "Any SSH box",
- "Works across every hooked agent, local or remote",
- "Remote sessions are ordinary rows",
- "One-click SSH setup",
- "remote machines over SSH",
- "this Mac or remote machines"
-];
-
-/** FRE-359 verdict B: no stale count, in any form.
- *
- * "every coding agent" and "Supported agents" left this list with FRE-997:
- * the page now lists all eighteen registry rows and `agents.test.ts` asserts
- * that equality, so the completeness claim is backed rather than incomplete. */
-const BANNED_COVERAGE = [
- "all thirteen",
- "thirteen coding agents",
- "13 agents"
-];
-
/** User-facing copy only: comments are not copy and may name the old wording. */
function copyOf(file: string): string {
return readFileSync(join(ROOT, file), "utf8")
@@ -83,44 +43,83 @@ function copyOf(file: string): string {
.replace(/^\s*\/\/.*$/gm, "");
}
-describe("remote claims stay off the site (FRE-323)", () => {
+describe("remote marketing reflects the supported setup (NOOK-1038)", () => {
+ it("fills the collage's missing sixth grid slot with host-labelled remote sessions", () => {
+ const collage = copyOf("src/components/nook/FeatureCollage.tsx");
+ // The hero spans two columns; four regular cards fill the other slots.
+ expect(collage.match(/ {
+ const breakdowns = copyOf("src/components/nook/FeatureBreakdowns.tsx");
+ expect(breakdowns).toContain('kicker="Remote"');
+ expect(breakdowns).toContain('title="Set up remote agents in one click"');
+ expect(breakdowns).toContain("Tailscale peer or an authorized host from your SSH config");
+ expect(breakdowns).toContain("");
+ });
+
+ it("demonstrates setup progressing to Live and a host-labelled session", () => {
+ const demos = copyOf("src/components/nook/MoreDemos.tsx");
+ for (const label of [
+ "connecting",
+ "pushing the hook binary",
+ "configuring agents",
+ "checking the host can reach this Mac",
+ "Live",
+ "Set up"
+ ])
+ expect(demos).toContain(label);
+ expect(demos).toContain('host="gpu-rig"');
+ });
+
for (const file of AUDITED) {
- it(`${file} does not restore a removed remote sentence`, () => {
- const copy = copyOf(file);
- for (const phrase of BANNED_REMOTE_CLAIMS) {
- expect(copy.toLowerCase()).not.toContain(phrase.toLowerCase());
- }
+ it(`${file} makes no blanket authorization promise`, () => {
+ const copy = copyOf(file).toLowerCase();
+ for (const phrase of [
+ "Any SSH box",
+ "Works across every hooked agent, local or remote",
+ "no SSH authorization needed"
+ ])
+ expect(copy).not.toContain(phrase.toLowerCase());
});
}
- it("/agents no longer offers remote sessions", () => {
- const copy = copyOf("src/routes/nook/agents.tsx");
- expect(copy).not.toContain("remote sessions report into the same grid");
- expect(copy).not.toContain("on this Mac or on remote");
- });
-
- it("the parked copy still exists, so restoring the story is a revert and not a rewrite", () => {
- const parked = readFileSync(join(ROOT, "marketing/drafts/remote-ssh/copy.md"), "utf8");
- for (const phrase of ["Agents on other machines", "Any SSH box, one click away"]) {
- expect(parked).toContain(phrase);
+ it("records the current decision without deleting the historical copy", () => {
+ for (const file of [
+ "marketing/drafts/remote-ssh/copy.md",
+ "marketing/drafts/remote-ssh/rationale.md",
+ "marketing/positioning.md"
+ ]) {
+ const text = readFileSync(join(ROOT, file), "utf8");
+ expect(text).toContain("NOOK-1038");
+ expect(text).toContain("Tailscale and authorized known hosts");
}
- expect(readFileSync(join(ROOT, "marketing/drafts/remote-ssh/rationale.md"), "utf8")).toContain(
- "FRE-320"
- );
+ expect(
+ readFileSync(join(ROOT, "marketing/drafts/remote-ssh/copy.md"), "utf8")
+ ).toContain("Any SSH box, one click away");
});
});
describe("the agent count is corrected, not dropped (FRE-997)", () => {
+ /** FRE-359 verdict B, minus the phrasings FRE-997 re-grounded in the full roster. */
+ const BANNED_COVERAGE = ["all thirteen", "thirteen coding agents", "13 agents"];
+
for (const file of AUDITED) {
it(`${file} states no stale count`, () => {
- const copy = copyOf(file);
- for (const phrase of BANNED_COVERAGE) {
- expect(copy.toLowerCase()).not.toContain(phrase.toLowerCase());
- }
+ const copy = copyOf(file).toLowerCase();
+ for (const phrase of BANNED_COVERAGE) expect(copy).not.toContain(phrase);
});
}
it("/agents states the registry's size", () => {
expect(copyOf("src/routes/nook/agents.tsx")).toContain("Supported agents");
});
+
+ it("the landing copy states the count the roster actually has", () => {
+ expect(copyOf("src/components/nook/FeatureBreakdowns.tsx")).toContain("18 agents");
+ });
});