2 Commits

Author SHA1 Message Date
6ff31346ff site: restore the remote SSH copy — HELD, do not merge (FRE-997, remote half)
DO NOT MERGE OR DEPLOY THIS BRANCH. It is the held half of FRE-997, split out
of a336a6c at the Code Review Engineer's changes-requested verdict and the
Chief of Staff's 2026-11-03 launch hold (comment 2ccad154), and it is stacked
on `freno-dev/FRE-997-count-18` so its own diff stays remote-only.

The copy it restores was pulled by FRE-323 (c1c498e, c718370). FRE-997's
argument is that those removals were checked against claims the copy never
made: hosts come from `~/.ssh/config` and Tailscale (`RemoteHostDiscovery`),
the one-time `ssh-copy-id` is a prerequisite the row hands the user as a
command (`RemoteHostStatus.fixCommand`), the per-host credential is minted and
copied over (`RemoteMirror`), and a remote host mirrors this Mac's integrations
(`RemoteMirrorReconcile`). Two things in the old copy were genuinely wrong and
are fixed rather than restored: the "every hooked agent" overstatement (Muse,
Cline and Amp are `.unsupported` for remote and are named) and the count.

It may land only when all of these are recorded on FRE-997:

1. a CTO verdict that the one-click flow was run on the shipping build;
2. footage of a remote agent joining the island;
3. the Chief of Staff revising the FRE-346 §3 launch gate and taking the
   public-claim call to the sponsor.

Until then the live site keeps the FRE-323 state. The restore condition this
commit deletes (`marketing/drafts/remote-ssh/copy.md`) is the gate being cited:
FRE-205 is todo/unassigned, FRE-320 is a verdict task, and no recording exists.

- The Remote collage tile and its mock.
- The "Any SSH box, one click away" breakdown with its RemoteHostDemo.
- The "One-click SSH setup" chip.
- The /agents remote paragraph and the Cline/Amp "this Mac only" rows.
- The approvals bullet names the three local-only agents instead of claiming
  every hooked agent.
- remote-claims.test.ts guards the restored copy plus the two overstatements
  that must not return.
- positioning.md moves remote SSH to publishable with its backing.

Gate on this branch, this run: `bun test src/routes/nook` 17 pass / 0 fail;
`bun run build` succeeds. Green tests do not lift the hold.
2026-11-04 00:30:21 +00:00
25ec84ceb4 site: state the agent count as eighteen (FRE-997, count half)
FRE-359 verdict B dropped the agent count instead of correcting it: the page
said thirteen while `AgentSpec.all` held eighteen, and the /agents table listed
13 of those rows. The count is a fact, not a claim, so this states it and makes
the table match the registry again.

- Landing island bullet and the marquee chip say 18 agents.
- /agents: title/description/ogTitle/H1 at eighteen, and the five missing rows
  are on the page — Kimi, GitHub Copilot CLI, Crush, Cursor, Amp — with events
  and gating taken from hook/setup.go, hook/normalize.go,
  hook/normalize_hosts.go, Sources/NookCore/Agents/HookConfigInstaller.swift
  and plugins/amp/index.ts. All five are Observe (kimi/copilot PreToolUse and
  PermissionRequest fire before the host's own check; Crush documents one
  event; a silent Cursor permission hook blocks; Amp's plugin API has no
  permission-wait event).
- agents.test.ts asserts the roster as an equality with the 18-name registry
  in registry order again, plus the count, instead of a subset check.
- remote-claims.test.ts keeps every FRE-323 remote removal guard and the parked
  copy, and drops only the two completeness bans the corrected count
  contradicts ("every coding agent", "Supported agents") — the ban existed
  because the page claimed the set and listed 13 of 18.
- positioning.md: the coverage row is publishable at 18.

This is the count/roster half of FRE-997 only. The remote-SSH restore is held
by the Chief of Staff against FRE-346 §3 and is not in this commit: no remote
tile, breakdown, demo, chip, /agents remote paragraph or drafts deletion here.

Gate: `bun test src/routes/nook` 18 pass / 0 fail; `bun run build` succeeds.
2026-11-04 00:21:35 +00:00
9 changed files with 407 additions and 257 deletions

View File

@@ -1,79 +0,0 @@
# Remote SSH copy — HELD, do not publish
**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.
## What was removed
### 1. Landing collage tile
Source: `src/components/nook/FeatureCollage.tsx` (removed 2026-10-29).
```
kicker: Remote
title: Agents on other machines
body: Run agents on Linux boxes and SSH servers — one click installs the
nook-hook, and their sessions join the same island.
```
The mock attached to it showed two session rows whose agent chips named
`build-box` and `gpu-rig`.
### 2. Landing feature breakdown section
Source: `src/components/nook/FeatureBreakdowns.tsx` (removed 2026-10-29).
```
kicker: Remote
title: Any SSH box, one click away
body: Pick a host from your ssh config or your tailnet and click once — the
nook-hook installs itself and its agents appear beside your local ones,
indistinguishable.
bullets:
- One click: connect, push the hook, configure agents, check it can reach this Mac
- Hosts mirror this Mac's installed integrations automatically
- Remote sessions are ordinary rows; the agent chip names the machine
```
The section's demo animated a `gpu-rig` host moving from "Not set up" through
`connecting` → `pushing the hook binary` → `configuring agents` → `checking the
host can reach this Mac` → `Live`, then showed a remote session row.
### 3. Approvals bullet
Source: `src/components/nook/FeatureBreakdowns.tsx` (removed 2026-10-29).
```
- Works across every hooked agent, local or remote
```
### 4. Feature chip
Source: `src/components/nook/IslandExtras.tsx` (removed 2026-10-29).
```
One-click SSH setup
```
## What replaced it
Nothing. The Remote tile and section were removed rather than replaced; the
existing Approvals cell already carries the verified local claim:
> 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.

View File

@@ -1,30 +0,0 @@
# Why the remote claim is parked
The site was selling a setup path a buyer does not get.
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.
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.
## What has to be true before it returns
| 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 |
Nothing in this folder is publishable. `marketing/approved/` remains the only
place a human publishes from.

View File

@@ -4,12 +4,13 @@ Every drafted asset draws its voice, claims, and constraints from this file. If
artifact invents its own framing, fix the artifact or fix this file — never both
silently diverging.
**Reconciled 2026-10-29** against the CTO's verdicts ([FRE-315], [FRE-322],
[FRE-354]), the Chief of Staff's price decision (2026-10-29 04:01), the Plan B
launch route ([FRE-346] → [FRE-203] `plan-b`), and `The-Nook` master (app floor is
**14.2**, not 14.0 — `freno-dev` `881190d`, site merge `134f41e`). The previous
version of this file named an upstream `the-nook/docs/marketing-strategy.md` that
no longer exists; the verdicts below are the source of truth it was pointing at.
**Reconciled 2026-10-29**, and again **2026-11-03** for the FRE-997 correction,
against the CTO's verdicts ([FRE-315], [FRE-322], [FRE-354], [FRE-997]), the Chief
of Staff's price decision (2026-10-29 04:01), the Plan B launch route ([FRE-346] →
[FRE-203] `plan-b`), and `The-Nook` master (app floor is **14.2**, not 14.0 —
`freno-dev` `881190d`, site merge `134f41e`). The previous version of this file
named an upstream `the-nook/docs/marketing-strategy.md` that no longer exists; the
verdicts below are the source of truth it was pointing at.
[FRE-315]: https://paperclip.freno.me/FRE/issues/FRE-315
[FRE-322]: https://paperclip.freno.me/FRE/issues/FRE-322
@@ -17,6 +18,7 @@ no longer exists; the verdicts below are the source of truth it was pointing at.
[FRE-320]: https://paperclip.freno.me/FRE/issues/FRE-320
[FRE-346]: https://paperclip.freno.me/FRE/issues/FRE-346
[FRE-203]: https://paperclip.freno.me/FRE/issues/FRE-203
[FRE-997]: https://paperclip.freno.me/FRE/issues/FRE-997
## The sentence
@@ -113,11 +115,16 @@ 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.
Remote SSH is **publishable**, and the site sells it again ([FRE-997]). The
copy that was pulled on 2026-10-29 had been checked against claims it never made:
hosts come from `~/.ssh/config` and Tailscale (`RemoteHostDiscovery`), the
one-time `ssh-copy-id` is a prerequisite the row hands the user as a command
(`RemoteHostStatus.fixCommand`), the per-host credential is minted and copied
over (`RemoteMirror`), and a remote host mirrors this Mac's integrations
(`RemoteMirrorReconcile`). Two things in the old copy were wrong and were fixed
rather than restored: the "every hooked agent" overstatement, and a count that
said thirteen against a registry of eighteen. Muse, Cline and Amp are
`.unsupported` for remote in `AgentSpec.remote`, and the copy names them.
### 5. Humans hold every account
@@ -125,7 +132,7 @@ No agent authenticates, posts, upvotes, or DMs. No sock puppets, no self-mention
without disclosure. `approved/` **is the only thing a human ever publishes from** —
nothing posts itself.
## Claims: status at 2026-10-29
## Claims: status at 2026-11-03
Each row names the evidence, not a feeling. Re-read the verdict before using a row.
@@ -136,16 +143,17 @@ Each row names the evidence, not a feeling. Re-read the verdict before using a r
| The hook and bridge are not a cloud service | **Verified code fact, held on disclosure** | [FRE-322] §2, wording corrected by [FRE-354] `(j)`. Held until Sentry is gated/removed and `/privacy` + `/faq` land. Never widen to "The Nook", never "nothing but your Mac". |
| Fan, thermal and charge control | **No claim verdict yet — do not lead with it** | Backed by `helper/` (privileged helper, SMC, no kext), but no CTO sentence exists for it. Documentary, not launch. |
| 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 ("thirteen agents", "8 agents", "all your agents") | **Excluded — verdict B, count removed** | [FRE-359] verdict B (2026-10-29): the page's 13 rows are the registry's first 13 of 18, so no number is publishable and `every` / `all` / `the agents The Nook supports` are banned. Removed from `agents.tsx`, the landing island section and the marquee. [FRE-322] §1 matrix for the sets. |
| 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, and `every` / `all` are still banned without naming the three local-only agents. |
| 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. |
| Remote agents over SSH, one click to install | **Publishable, with the three local-only agents named** | [FRE-997]: `RemoteHostDiscovery` (ssh config + tailnet), `RemoteMirror` (hook push, credential, mirroring), `RemoteMirrorReconcile`, `RemoteHostStatus.fixCommand` (the one-time `ssh-copy-id` the row shows). `AgentSpec.remote` is `.unsupported` for Muse, Cline and Amp — the landing bullet and /agents paragraph name them; never say "every hooked agent". |
| "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. |
## Voice
- Short, concrete, no hype adjectives. Numbers over adjectives — but no coverage
counts (see the excluded row above).
- Short, concrete, no hype adjectives. Numbers over adjectives — the only count
that ships is eighteen, and it is the registry's own size (see the coverage row
above).
- The notch is the product's home; agents are the product's point.
- Never apologize for being a utility. The utility is the point.
- Banned phrasings: competitor names (see rule 1), "AI-powered", "revolutionary",

View File

@@ -1,5 +1,6 @@
import {
For,
Show,
createEffect,
createSignal,
onCleanup,
@@ -20,6 +21,7 @@ import { Campfire, type CampfireState } from "./Campfire";
import {
ApprovalCardMock,
SessionRowMock,
Glyph,
CLAUDE,
CODEX
} from "./ModuleMocks";
@@ -35,7 +37,7 @@ import {
* Detailed feature breakdowns: alternating copy + live-demo sections.
* Each demo stages the real interaction loop: attention flares, approval
* resolves, fans throttle, the charge ceiling holds, meetings surface,
* the camera wakes. Newer demos live in
* the camera wakes, a remote host goes live. Newer demos live in
* MoreDemos.tsx; app-anatomy pieces in ModuleMocks.tsx.
*/
@@ -805,6 +807,112 @@ function CameraDemo() {
);
}
/* ── Demo 7: Settings ▸ Remote hosts — one click to Live ──────────── */
/** RemoteMirror's progress steps, in order. */
const SETUP_STEPS = [
"connecting",
"pushing the hook binary",
"configuring agents",
"checking the host can reach this Mac"
];
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 live = () => step() === 4;
const working = () => step() >= 0 && step() < 4;
const chip = () =>
live()
? { color: "rgb(48,209,88)", 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 (
<div class="relative mx-auto w-full max-w-[360px] space-y-3">
{/* the host's card in Settings ▸ Agents ▸ Remote hosts */}
<ModuleCard>
<div class="space-y-2 text-left">
<div class="flex items-center gap-2.5">
<div class="min-w-0 flex-1">
<p class="text-[13px] font-semibold text-white">gpu-rig</p>
<p class="truncate text-[11px] text-white/45">tailnet peer</p>
</div>
<Show when={working()}>
<span class="h-3 w-3 animate-spin rounded-full border-[1.5px] border-white/25 border-t-white/80" />
</Show>
<span class="flex items-center gap-1.5 text-[10.5px] font-medium text-white/85">
<span
class="h-[7px] w-[7px] rounded-full transition-colors duration-300"
style={{ background: chip().color }}
/>
{chip().label}
</span>
<Show
when={live()}
fallback={
<span
class="rounded-md px-2.5 py-1 text-[11px] font-semibold text-white transition-all duration-200"
style={{
background: ACCENT,
opacity: working() ? 0 : 1,
transform: pressed() ? "scale(0.92)" : "scale(1)"
}}
>
Set up
</span>
}
>
<Glyph name="refresh" size={11} class="text-white/50" />
<span class="rounded-md bg-white/10 px-2 py-1 text-[11px] font-semibold text-[#ff6961]">
Remove
</span>
</Show>
</div>
<p class="h-4 text-[11px] text-white/50">
{working() ? SETUP_STEPS[step()] : " "}
</p>
</div>
</ModuleCard>
{/* its agents arrive as ordinary rows, the machine named in the chip */}
<div
class="transition-all duration-500"
style={{
opacity: live() ? 1 : 0,
transform: live() ? "translateY(0)" : "translateY(6px)"
}}
>
<SessionRowMock
title="trainer · fp16"
prompt="benchmark fp16"
state="Running python bench.py…"
status="running"
agent={CODEX}
host="gpu-rig"
origin="iTerm2"
age="now"
/>
</div>
<FakeCursor
phase={pressed() ? "press" : step() === -1 ? "idle" : "live"}
at={{ x: 88, y: 16 }}
/>
</div>
);
}
/* ── Demo 8: campfire states ───────────────────────────────────────── */
function CampfireDemo() {
@@ -851,7 +959,7 @@ export default function FeatureBreakdowns() {
title="Your whole fleet, in one pixel strip"
body="Every session, every agent, one square each. The pill never wraps, never grows — an arriving question flashes the fire and swaps the slot, then hands the notch back."
bullets={[
"Claude Code, Codex, Gemini CLI, OpenCode, Pi, Muse, Cursor and more",
"18 agents: Claude Code, Codex, Gemini CLI, OpenCode, Pi, Muse, and more",
"Custom agents: your own coding agent can wire itself in from the published contract",
"Slot rankings — you choose which data earns the notch",
"You don't need a notch — external displays get the same island, floating top-center"
@@ -868,6 +976,7 @@ export default function FeatureBreakdowns() {
"Approval cards pin above the session list; questions answer inside their row",
"Allow for session waves through the rest of that session's requests",
"Answer from iPhone or Apple Watch too, now in TestFlight beta",
"Works across local and remote sessions; Muse, Cline and Amp are this Mac only"
]}
reversed
>
@@ -887,6 +996,20 @@ export default function FeatureBreakdowns() {
<TerminalReplyDemo />
</BreakdownSection>
<BreakdownSection
kicker="Remote"
title="Any SSH box, one click away"
body="Pick a host from your ssh config or your tailnet and click once — the nook-hook installs itself and its agents appear beside your local ones, indistinguishable."
bullets={[
"One click: connect, push the hook, configure agents, check it can reach this Mac",
"Hosts mirror this Mac's installed integrations automatically",
"Remote sessions are ordinary rows; the agent chip names the machine"
]}
reversed
>
<RemoteHostDemo />
</BreakdownSection>
<BreakdownSection
kicker="The campfire"
title="A hearth that tells the truth"

View File

@@ -1,6 +1,12 @@
import { For, createSignal, onCleanup, onMount, type JSX } from "solid-js";
import { IslandPill, STATUS, CALM, CRITICAL, ModuleCard } from "./IslandMock";
import { ApprovalCardMock, DieLine, CLAUDE } from "./ModuleMocks";
import {
ApprovalCardMock,
SessionRowMock,
DieLine,
CLAUDE,
CODEX
} from "./ModuleMocks";
/**
* Feature collage: five cells sold as the app's actual UI. The hero cell
@@ -99,6 +105,35 @@ function FansPanelMock() {
);
}
/**
* Remote agents as the panel draws them: ordinary session rows whose agent
* chip names the machine ("Claude Code · build-box").
*/
function RemoteMock() {
return (
<div class="space-y-1.5">
<SessionRowMock
title="api · main"
prompt="fix the flaky test"
state="Running pytest -x…"
status="running"
agent={CLAUDE}
host="build-box"
age="4m"
/>
<SessionRowMock
title="trainer · fp16"
prompt="benchmark fp16"
state="Done · replied 1m ago"
status="ready"
agent={CODEX}
host="gpu-rig"
age="12m"
/>
</div>
);
}
/* Deterministic usage wander for the traces. */
const wander = (n: number, k: number) =>
0.42 + 0.22 * Math.sin(n * 0.45 + k * 1.7) + 0.12 * Math.sin(n * 1.3 + k);
@@ -221,6 +256,19 @@ export default function FeatureCollage() {
</InkStage>
</CollageCell>
{/* Remote agents */}
<CollageCell
kicker="Remote"
title="Agents on other machines"
body="Run agents on Linux boxes and SSH servers — one click installs the nook-hook, and their sessions join the same island."
>
<InkStage class="w-full">
<div class="w-full px-3 text-left">
<RemoteMock />
</div>
</InkStage>
</CollageCell>
{/* Gauges */}
<CollageCell
kicker="Your Mac"

View File

@@ -722,7 +722,7 @@ function LayoutMock() {
/* ── Marquee ─────────────────────────────────────────────────────── */
const CHIPS_A = [
"Agents wired in",
"18 agents",
"Custom agents",
"Six chiptunes",
"Focus-aware slots",
@@ -740,6 +740,7 @@ const CHIPS_B = [
"tmux · iTerm2 · kitty",
"Markdown replies",
"Tailscale hosts",
"One-click SSH setup",
"iPhone widgets",
"Watch-face widgets",
"Screenshots to clipboard",

View File

@@ -1,6 +1,5 @@
/**
* Regression test for the /agents roster (NOOK-336) and its coverage wording
* (FRE-359).
* Regression test for the /agents roster and its coverage count.
*
* The page mirrors the app's registry, `AgentSpec.all` in
* the-nook/Sources/NookCore/Agents/AgentRegistry.swift. When Muse joined
@@ -9,11 +8,11 @@
* them. The registry lives in another repo, so the expected roster is
* restated here; when an agent is added there, add it here and on the page.
*
* FRE-359 verdict B: the page no longer states a count or claims coverage
* ("all thirteen", "every coding agent The Nook supports"). The page still
* lists 13 of the registry's 18 rows — the table is hand-mirrored and its
* regeneration is its own ticket — so the roster check here is a subset
* check in registry order, not an equality check.
* FRE-997 restored the count. The page said thirteen while the registry held
* eighteen, so the count was dropped rather than corrected (FRE-359 verdict B);
* now that all eighteen rows are on the page, the roster check is an equality
* check again and the page states eighteen. A subset check would let a shipped
* agent go missing behind a passing suite, which is the NOOK-336 failure.
*
* STATIC SOURCE AUDIT, the same pattern as the downloads regressions.
*/
@@ -26,6 +25,9 @@ const SOURCE = readFileSync(join(import.meta.dir, "agents.tsx"), "utf8");
/** User-facing copy only: comments are not copy and may name the old wording. */
const COPY = SOURCE.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
/** JSX wraps prose across lines; match a sentence, not its indentation. */
const PROSE = COPY.replace(/\s+/g, " ");
/** AgentSpec.all display names, in registry order (18 rows). @ The-Nook f4258034, 2026-10-29 */
const REGISTRY = [
"Claude Code",
@@ -48,51 +50,41 @@ const REGISTRY = [
"Amp"
];
const NUMBER_WORDS = ["twelve", "thirteen", "fourteen", "fifteen", "sixteen", "seventeen", "eighteen"];
const entries = [...SOURCE.matchAll(/^\s{4}name: "([^"]+)",$/gm)].map((m) => m[1]);
/** The page's rows are a subset of the registry, in registry order. */
function subsetInOrder(page: string[], registry: string[]): boolean {
let cursor = 0;
for (const name of page) {
const at = registry.indexOf(name, cursor);
if (at === -1) return false;
cursor = at + 1;
}
return true;
}
describe("Nook /agents roster (NOOK-336)", () => {
it("lists only registry agents, in registry order", () => {
expect(entries.length).toBeGreaterThan(0);
expect(subsetInOrder(entries, REGISTRY)).toBe(true);
describe("Nook /agents roster", () => {
it("lists every registry agent, once, in registry order", () => {
expect(entries).toEqual(REGISTRY);
});
it("lists each agent at most once", () => {
expect(new Set(entries).size).toBe(entries.length);
it("states the registry's size", () => {
expect(entries.length).toBe(18);
expect(COPY).toContain("all eighteen");
expect(COPY).toContain("The 18 agents");
expect(COPY).toContain("eighteen coding agents");
});
it("carries no stale count", () => {
expect(COPY.toLowerCase()).not.toContain("thirteen");
expect(COPY).not.toMatch(/\b13 agents\b/);
expect(COPY).not.toContain("all thirteen");
});
});
describe("Nook /agents coverage wording (FRE-359)", () => {
it("states no agent count anywhere on the page", () => {
for (const word of NUMBER_WORDS) {
expect(COPY.toLowerCase()).not.toContain(word);
}
// A bare numeral count ("13 agents", "The 13") is just as precise and just
// as unverified; the page must not state one in any form.
expect(COPY).not.toMatch(/\b\d{1,2} agents\b/);
expect(COPY).not.toMatch(/\ball \d{1,2}\b/);
describe("Nook /agents remote coverage", () => {
it("says agents can run on remote machines over SSH", () => {
expect(PROSE).toContain("remote machines");
expect(PROSE).toContain("remote sessions report into the same grid");
});
it("makes no completeness claim over the agent set", () => {
for (const banned of [
"every coding agent",
"the agents the nook supports",
"agents the nook supports",
"supported agents"
]) {
expect(COPY.toLowerCase()).not.toContain(banned);
}
it("names the three agents that stay on this Mac", () => {
expect(PROSE).toContain("Muse, Cline and Amp run on this Mac only");
});
it("discloses the local-only rows where they are listed", () => {
const localOnly = [
...SOURCE.matchAll(/This Mac only — remote hosts skip it/g)
];
expect(localOnly.length).toBe(3);
});
});

View File

@@ -1,11 +1,10 @@
/**
* `/agents` on the Nook subdomain — the agent roster and each row's event
* coverage. It states no agent count and claims no completeness over the
* registry (FRE-359 verdict B).
* `/agents` on the Nook subdomain — all eighteen agents in the registry and
* each row's event coverage.
*
* The registry below mirrors `AgentSpec.all` in the app
* (~/Code/the-nook/Sources/NookCore/Agents/AgentSpec.swift) and each agent's
* installed hook surface (hook/setup.go) or plugin event subscription
* (~/Code/the-nook/Sources/NookCore/Agents/AgentRegistry.swift) and each
* agent's installed hook surface (hook/setup.go) or plugin event subscription
* (plugins/*), normalized onto the shared event vocabulary
* (~/Code/the-nook/docs/agent-interface.md). Copy follows
* marketing/positioning.md: category nouns, honest scoping, no unverifiable
@@ -210,7 +209,8 @@ const AGENTS: AgentEntry[] = [
},
{
name: "Cline",
wiring: "One hook file per event in its hooks directory",
wiring:
"One hook file per event in its hooks directory. This Mac only — remote hosts skip it",
events: [
"SessionStart",
"SessionEnd",
@@ -261,6 +261,93 @@ const AGENTS: AgentEntry[] = [
],
gating:
"Tool calls can be allowed or denied from a card; permission prompts stay with the agent"
},
{
name: "Kimi",
wiring: "Hook rules in its TOML config, one per event",
events: [
"SessionStart",
"SessionEnd",
"UserPromptSubmit",
"AgentStart",
"PreToolUse",
"PermissionRequest",
"PermissionResolved",
"PostToolUse",
"PostToolUseFailure",
"Stop",
"SessionError",
"SubagentStart",
"SubagentStop",
"PreCompact",
"Notification"
],
gating:
"Observation — every event appears as a card; its PreToolUse fires before its own permission check, so the agent keeps its own prompting"
},
{
name: "GitHub Copilot CLI",
wiring: "One hook file of ours in its hooks directory",
events: [
"SessionStart",
"SessionEnd",
"UserPromptSubmit",
"PreToolUse",
"PermissionRequest",
"PostToolUse",
"PostToolUseFailure",
"Stop",
"SubagentStart",
"SubagentStop",
"ErrorOccurred",
"PreCompact",
"Notification"
],
gating:
"Observation — every event appears as a card; its permission request runs before its own policy check, so the agent keeps its own prompting"
},
{
name: "Crush",
wiring:
"One entry in its JSONC config, the only event it documents",
events: ["PreToolUse"],
gating:
"Observation — tool calls appear as cards; its own permission prompts decide"
},
{
name: "Cursor",
wiring:
"Hook entries in its hooks.json, merged with your own. No permission hook, on purpose",
events: [
"SessionStart",
"SessionEnd",
"UserPromptSubmit",
"PostToolUse",
"PostToolUseFailure",
"Stop",
"SessionError",
"AfterAgentResponse",
"SubagentStop",
"PreCompact"
],
gating:
"Observation — a permission hook that stays silent blocks the action here, so Cursor keeps its own prompting"
},
{
name: "Amp",
wiring:
"In-process extension package. This Mac only — remote hosts skip it",
events: [
"SessionStart",
"SessionEnd",
"UserPromptSubmit",
"PreToolUse",
"PostToolUse",
"Stop",
"SessionError"
],
gating:
"Observation — its plugin API has no permission-wait event, so the agent keeps its own prompting"
}
];
@@ -284,25 +371,27 @@ export default function NookAgentsPage() {
<>
<EdgeCacheHeaders maxAge={300} />
<PageHead
title="Agent coverage"
description="The coding agents The Nook connects to, with the events each hook reports and what you can do from a card: watch sessions live, answer permission prompts, unblock work."
ogTitle="The coding agents The Nook connects to, and what each hook sees"
title="Supported agents"
description="Every coding agent The Nook supports — all eighteen, with each hook's exact event coverage and what you can do from a card: watch sessions live, answer permission prompts, unblock work."
ogTitle="The 18 agents The Nook supports, and what each hook sees"
/>
<SubdomainHeader />
<main class="min-h-screen px-[8vw] py-[8vh]">
<h1 class="text-text mb-3 text-3xl font-bold tracking-tight sm:text-4xl">
Agent coverage
Supported agents
</h1>
<p class="text-subtext0 mb-2 max-w-2xl text-lg">
The Nook watches coding agents in one place — your menu bar and notch
show sessions that are running, thinking, or blocked, and a card can
answer a permission prompt without leaving your keyboard.
The Nook watches eighteen coding agents in one place — your menu bar and
notch show every session that is running, thinking, or blocked, and a
card answers permission prompts without leaving your keyboard.
</p>
<p class="text-subtext1 mb-10 max-w-2xl text-sm">
Each agent reports its own set of events. "Allow / Deny from a card"
means the agent hands the decision to The Nook while it waits;
"Observe" means every event still appears live, but the agent keeps
its own prompting.
its own prompting. Agents can run on this Mac or on remote machines
over SSH — remote sessions report into the same grid. Muse, Cline and
Amp run on this Mac only.
</p>
<div class="grid gap-4 lg:grid-cols-2">

View File

@@ -1,31 +1,27 @@
/**
* Regression test for the removed remote / SSH claims and the removed agent
* count (FRE-323, FRE-359 verdict B).
* Guard for the remote / SSH story on the Nook site, and for the two phrasings
* that must not come back with it (FRE-323 → FRE-997).
*
* 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.
* FRE-323 removed the remote copy because it had been checked against claims
* the copy never made: the app does list hosts from `~/.ssh/config` and
* Tailscale, a one-time `ssh-copy-id` is a normal prerequisite the row explains,
* and the per-host credential is minted and copied automatically
* (`RemoteHostDiscovery`, `RemoteHostStatus.fixCommand`, `RemoteMirror`). Two
* things in the old copy were genuinely wrong, and this file guards those
* instead of the copy as a whole:
*
* 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.
* - "Works across every hooked agent, local or remote" overstated it. Muse,
* Cline and Amp are `.unsupported` for remote in `AgentSpec.remote`, so the
* landing bullet and the /agents paragraph name them.
* - The count said thirteen while `AgentSpec.all` holds eighteen. The fix was
* to say eighteen, not to drop the count.
*
* So the removal guards from `d44133f` are gone: a removed sentence is exactly
* what this task restores. What stays is the pair of bans, because either one
* coming back would repeat the original defect.
*
* 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";
@@ -33,7 +29,7 @@ import { join } from "node:path";
const ROOT = join(import.meta.dir, "../../..");
/** Shipped page and component source. Tests and drafts may quote the old copy. */
/** Shipped page and component source. */
const AUDITED = [
"src/routes/nook/index.tsx",
"src/routes/nook/agents.tsx",
@@ -43,25 +39,15 @@ const AUDITED = [
"src/components/nook/IslandExtras.tsx"
];
/** Every sentence FRE-323 ordered off the site. */
/** The overstatements the restored copy must never carry again. */
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 count, no coverage claim, in any form. */
const BANNED_COVERAGE = [
"all thirteen",
"thirteen coding agents",
// a universal claim: three registry agents cannot mirror over SSH
"every hooked agent, local or remote",
"Works across every hooked agent",
// the stale count this task replaced
"13 agents",
"every coding agent",
"Supported agents"
"all thirteen",
"thirteen coding agents"
];
/** User-facing copy only: comments are not copy and may name the old wording. */
@@ -71,9 +57,46 @@ function copyOf(file: string): string {
.replace(/^\s*\/\/.*$/gm, "");
}
describe("remote claims stay off the site (FRE-323)", () => {
describe("the restored remote copy is on the site (FRE-997)", () => {
it("the landing collage sells agents on other machines", () => {
const copy = copyOf("src/components/nook/FeatureCollage.tsx");
for (const phrase of [
"Agents on other machines",
"one click installs the nook-hook",
"Remote"
]) {
expect(copy).toContain(phrase);
}
});
it("the landing breakdown keeps the SSH section and its demo", () => {
const copy = copyOf("src/components/nook/FeatureBreakdowns.tsx");
for (const phrase of [
"Any SSH box, one click away",
"Pick a host from your ssh config or your tailnet and click once",
"Remote sessions are ordinary rows",
"RemoteHostDemo"
]) {
expect(copy).toContain(phrase);
}
});
it("the chip is back", () => {
expect(copyOf("src/components/nook/IslandExtras.tsx")).toContain(
"One-click SSH setup"
);
});
it("/agents says remote sessions report into the same grid", () => {
const copy = copyOf("src/routes/nook/agents.tsx");
expect(copy).toContain("remote machines");
expect(copy).toContain("remote sessions report into the same grid");
});
});
describe("the two original overstatements stay off the site (FRE-323, FRE-359)", () => {
for (const file of AUDITED) {
it(`${file} does not restore a removed remote sentence`, () => {
it(`${file} claims no universal remote coverage and no stale count`, () => {
const copy = copyOf(file);
for (const phrase of BANNED_REMOTE_CLAIMS) {
expect(copy.toLowerCase()).not.toContain(phrase.toLowerCase());
@@ -81,34 +104,9 @@ describe("remote claims stay off the site (FRE-323)", () => {
});
}
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);
}
expect(readFileSync(join(ROOT, "marketing/drafts/remote-ssh/rationale.md"), "utf8")).toContain(
"FRE-320"
it("the landing names the agents that stay local", () => {
expect(copyOf("src/components/nook/FeatureBreakdowns.tsx")).toContain(
"Muse, Cline and Amp are this Mac only"
);
});
});
describe("agent coverage count stays off the page (FRE-359 verdict B)", () => {
for (const file of AUDITED) {
it(`${file} states no coverage count`, () => {
const copy = copyOf(file);
for (const phrase of BANNED_COVERAGE) {
expect(copy.toLowerCase()).not.toContain(phrase.toLowerCase());
}
});
}
it("/agents titles the table Agent coverage", () => {
expect(copyOf("src/routes/nook/agents.tsx")).toContain("Agent coverage");
});
});