diff --git a/marketing/positioning.md b/marketing/positioning.md index 174e42d..0922cf7 100644 --- a/marketing/positioning.md +++ b/marketing/positioning.md @@ -4,8 +4,9 @@ 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 +**Reconciled 2026-10-29**, and again **2026-11-03** for the FRE-997 count +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 @@ -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 @@ -125,7 +127,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,7 +138,7 @@ 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. | | 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. | | "Nothing leaves your Mac", "no telemetry", "local-only", "no account" | **False today — banned** | Sentry unconditional, licence check with hardware fingerprint ([FRE-322] §2.4). | @@ -144,8 +146,9 @@ Each row names the evidence, not a feeling. Re-read the verdict before using a r ## 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", diff --git a/src/components/nook/FeatureBreakdowns.tsx b/src/components/nook/FeatureBreakdowns.tsx index cfbae76..fa1d546 100644 --- a/src/components/nook/FeatureBreakdowns.tsx +++ b/src/components/nook/FeatureBreakdowns.tsx @@ -851,7 +851,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" diff --git a/src/components/nook/IslandExtras.tsx b/src/components/nook/IslandExtras.tsx index dd06b16..77e6d62 100644 --- a/src/components/nook/IslandExtras.tsx +++ b/src/components/nook/IslandExtras.tsx @@ -722,7 +722,7 @@ function LayoutMock() { /* ── Marquee ─────────────────────────────────────────────────────── */ const CHIPS_A = [ - "Agents wired in", + "18 agents", "Custom agents", "Six chiptunes", "Focus-aware slots", diff --git a/src/routes/nook/agents.test.ts b/src/routes/nook/agents.test.ts index 94830a5..f5021df 100644 --- a/src/routes/nook/agents.test.ts +++ b/src/routes/nook/agents.test.ts @@ -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. */ @@ -48,51 +47,23 @@ 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); - }); -}); - -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/); - }); - - 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("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"); }); }); diff --git a/src/routes/nook/agents.tsx b/src/routes/nook/agents.tsx index c43ff93..ccc2672 100644 --- a/src/routes/nook/agents.tsx +++ b/src/routes/nook/agents.tsx @@ -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 @@ -261,6 +260,92 @@ 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", + 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,19 +369,19 @@ export default function NookAgentsPage() { <>

- Agent coverage + Supported agents

- 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.

Each agent reports its own set of events. "Allow / Deny from a card" diff --git a/src/routes/nook/remote-claims.test.ts b/src/routes/nook/remote-claims.test.ts index f15f6bb..9dee6fd 100644 --- a/src/routes/nook/remote-claims.test.ts +++ b/src/routes/nook/remote-claims.test.ts @@ -11,6 +11,16 @@ * 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-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. + * * 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 @@ -55,13 +65,15 @@ const BANNED_REMOTE_CLAIMS = [ "this Mac or remote machines" ]; -/** FRE-359 verdict B: no count, no coverage claim, in any form. */ +/** 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", - "every coding agent", - "Supported agents" + "13 agents" ]; /** User-facing copy only: comments are not copy and may name the old wording. */ @@ -98,9 +110,9 @@ describe("remote claims stay off the site (FRE-323)", () => { }); }); -describe("agent coverage count stays off the page (FRE-359 verdict B)", () => { +describe("the agent count is corrected, not dropped (FRE-997)", () => { for (const file of AUDITED) { - it(`${file} states no coverage count`, () => { + it(`${file} states no stale count`, () => { const copy = copyOf(file); for (const phrase of BANNED_COVERAGE) { expect(copy.toLowerCase()).not.toContain(phrase.toLowerCase()); @@ -108,7 +120,7 @@ describe("agent coverage count stays off the page (FRE-359 verdict B)", () => { }); } - it("/agents titles the table Agent coverage", () => { - expect(copyOf("src/routes/nook/agents.tsx")).toContain("Agent coverage"); + it("/agents states the registry's size", () => { + expect(copyOf("src/routes/nook/agents.tsx")).toContain("Supported agents"); }); });