Public: anyone can read this; the crew can change it.
docs/features/client-forge.md walks a zero-technical client from one link to the crew at work, and a test follows every link in it #10734
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/10701-docs-features-client-forge-md-walks-a-ze → main 1 commit fork
2 files
changed +76
−0
docs/features/client-forge.md added +42
| @@ -0,0 +1,42 @@ | ||
| 1 | # A forge for a client, from one link | |
| 2 | ||
| 3 | This page is for the person who sends a client a forge and for the planner who | |
| 4 | decides what is missing. The client sees apps and a crew working on them, in | |
| 5 | plain words. The sender makes the forge, sends one link and the client takes | |
| 6 | it from there. Each step names the route or MCP tool that does it. | |
| 7 | ||
| 8 | ## Make the forge | |
| 9 | ||
| 10 | The sender opens `/forges/new`, names the forge, picks the apps and presses | |
| 11 | Spawn. An assistant does the same with MCP tool `spawn`. The answer holds one | |
| 12 | claim link, shown once. `/forges` and MCP tool `forges` list the forges sent. | |
| 13 | ||
| 14 | ## Send the link | |
| 15 | ||
| 16 | The sender copies the claim link and sends it. The client opens `/claim` | |
| 17 | (the link carries a fragment that never reaches a log) and makes an account with a username and a password, or a passkey. | |
| 18 | ||
| 19 | ## First login | |
| 20 | ||
| 21 | The client lands on `/`. The page shows a card "Your next steps"; once the crew has a | |
| 22 | direction, the card says it is at work and lists its lines. The card has three steps: make an | |
| 23 | everyday account, open your apps, tell the crew something. Each step is ticked | |
| 24 | from what happened, never by hand. MCP tool `next_steps` reads the same three | |
| 25 | steps. `/apps` lists the apps and `/feed` lists what the crew did. | |
| 26 | ||
| 27 | ## Tell the crew from an app | |
| 28 | ||
| 29 | The client opens an app at `/:owner/:name` and writes one sentence at | |
| 30 | `/:owner/:name/tell`, or in the box on `/talk`. MCP tool `tell` does the | |
| 31 | same. The crew answers on the app's page; `/:owner/:name/runs` and MCP tool | |
| 32 | `runs` show each piece of work in progress. | |
| 33 | ||
| 34 | ## What the client never sees | |
| 35 | ||
| 36 | Settings, tokens, run logs and git words belong to the admin. The client sees | |
| 37 | apps, the crew's lines and the box to tell them something. | |
| 38 | ||
| 39 | - No door yet: a client account that hides settings and technical words. | |
| 40 | Today the client is the forge's admin and sees every page. | |
| 41 | - No door yet: a first page that shows the crew working before the client has | |
| 42 | told it anything. |
test/client-forge-doc.test.ts added +34
| @@ -0,0 +1,34 @@ | ||
| 1 | import { expect, test } from "bun:test"; | |
| 2 | import { readFileSync } from "node:fs"; | |
| 3 | import { join } from "node:path"; | |
| 4 | import { knownPatterns } from "../src/http/server.ts"; | |
| 5 | import { TOOLS } from "../src/http/mcp/tools.ts"; | |
| 6 | ||
| 7 | const path = join(import.meta.dir, "..", "docs/features/client-forge.md"); | |
| 8 | const doc = readFileSync(path, "utf8"); | |
| 9 | ||
| 10 | const routes = [...new Set([...doc.matchAll(/`(\/[^`\s]*)`/g)].map((m) => m[1]!))]; | |
| 11 | const tools = [...new Set([...doc.matchAll(/MCP tool\s+`([a-z_]+)`/g)].map((m) => m[1]!))]; | |
| 12 | ||
| 13 | test("the page stays under 60 lines", () => { | |
| 14 | expect(doc.trimEnd().split("\n").length).toBeLessThan(60); | |
| 15 | }); | |
| 16 | ||
| 17 | test("the page names routes and tools to follow", () => { | |
| 18 | expect(routes.length).toBeGreaterThan(5); | |
| 19 | expect(tools.length).toBeGreaterThan(3); | |
| 20 | }); | |
| 21 | ||
| 22 | test("every route the page names is in the router", () => { | |
| 23 | const known = new Set(knownPatterns()); | |
| 24 | for (const r of routes) expect(known.has(r), `route ${r}`).toBe(true); | |
| 25 | }); | |
| 26 | ||
| 27 | test("every MCP tool the page names is in the tool list", () => { | |
| 28 | const names = new Set(TOOLS.map((t) => t.name)); | |
| 29 | for (const t of tools) expect(names.has(t), `tool ${t}`).toBe(true); | |
| 30 | }); | |
| 31 | ||
| 32 | test("a step with no door says so in one line", () => { | |
| 33 | expect(doc).toContain("No door yet:"); | |
| 34 | }); |