@hobocode/thought-layer 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -20,8 +20,9 @@ This is open source and BYOK by design. The point is to help people build real t
20
20
 
21
21
  **A Pi package** that adds, on top of the skills:
22
22
 
23
- - **Deterministic tools** the agent can call so the math is exact and never re-derived: `tl_score` (confidence to status and grade), `tl_domains` (availability, BYOK), `tl_project` (the numeric business projection).
24
- - **Slash commands** (prompt templates): `/tl` runs the whole flow, and `/tl-panel`, `/tl-grill`, `/tl-prd`, `/tl-naming` run each stage.
23
+ - **Deterministic tools** the agent can call so the math is exact and never re-derived: `tl_score` (confidence to status and grade), `tl_domains` (availability, BYOK), `tl_project` (the numeric business projection), `tl_state` (the portable progress file), and `tl_scaffold` (a deterministic, deployable static site from the spec + brand).
24
+ - **Slash commands** (prompt templates): `/tl` runs the whole flow; `/tl-speedrun` is the fast unranked path; `/tl-panel`, `/tl-grill`, `/tl-prd`, `/tl-naming` run each stage; `/tl-build` builds the hardened PRD into a deploy-ready artifact.
25
+ - **A `tl` CLI** for any shell agent (`npx -y @hobocode/thought-layer tl ...`): `read`/`list`/`answer`/`feedback`/`artifact`/`cursor`/`export` for the shared progress file, and `scaffold` for the deployable static-site floor.
25
26
 
26
27
  ## Install
27
28
 
@@ -61,8 +62,8 @@ The hosted version of the rigor lives at [weareallproductmanagersnow.com](https:
61
62
 
62
63
  ## Roadmap
63
64
 
64
- - **Done:** the rigor as portable skills, and a Pi package with deterministic tools + slash commands.
65
- - **Phase 3:** a `build` step that turns the PRD into a deploy-ready artifact, built by your own agent.
65
+ - **Done:** the rigor as portable skills; a Pi package with deterministic tools + slash commands; the portable progress file (`tl_state` / the `tl` CLI) shared with the web app; and the speedrun.
66
+ - **Phase 3 (done):** a `build` step that turns the hardened PRD into a deploy-ready artifact, built by your own agent (`/tl-build`), with a deterministic `tl_scaffold` tool that writes an instantly-deployable branded static site as the floor.
66
67
  - **Phase 4:** a `deploy` step that publishes it to a live URL you own (Netlify deploy-and-claim by default), closing the loop.
67
68
 
68
69
  ## Notes for contributors
package/core/index.ts CHANGED
@@ -15,3 +15,5 @@ export * from "./stages.ts";
15
15
  export * from "./stage-map.ts";
16
16
  export * from "./state-file.ts";
17
17
  export * from "./state-ops.ts";
18
+ export * from "./scaffold.ts";
19
+ export * from "./scaffold-io.ts";
@@ -0,0 +1,56 @@
1
+ // Node IO for the deterministic scaffold, shared by both frontends (the
2
+ // tl_scaffold Pi tool and the `tl scaffold` CLI). The pure generation lives in
3
+ // scaffold.ts; this loads the state file, writes the site to a publish dir, and
4
+ // writes the build.json manifest co-located with the selected state file.
5
+
6
+ import { mkdirSync, writeFileSync } from "node:fs";
7
+ import { dirname, isAbsolute, join, resolve } from "node:path";
8
+ import { loadStateFile } from "./state-file.ts";
9
+ import { extractScaffoldSpec, buildStarterSite, scaffoldManifest, type ScaffoldOptions } from "./scaffold.ts";
10
+ import type { StateOpResult } from "./state-ops.ts";
11
+
12
+ export interface ScaffoldRunOptions extends ScaffoldOptions {
13
+ path?: string;
14
+ outDir?: string;
15
+ }
16
+
17
+ export function runScaffold(opts: ScaffoldRunOptions, ctx: { builtAt: string }): StateOpResult {
18
+ try {
19
+ const loaded = loadStateFile(opts.path);
20
+ const spec = extractScaffoldSpec(loaded.state);
21
+ const { files } = buildStarterSite(spec, { domain: opts.domain, founderName: opts.founderName, socialImage: opts.socialImage });
22
+
23
+ const outDir = opts.outDir || "dist";
24
+ const outAbs = isAbsolute(outDir) ? outDir : resolve(process.cwd(), outDir);
25
+ mkdirSync(outAbs, { recursive: true });
26
+ for (const [name, content] of Object.entries(files)) {
27
+ writeFileSync(join(outAbs, name), content);
28
+ }
29
+
30
+ const prd = loaded.state.prd && typeof loaded.state.prd === "object" ? (loaded.state.prd as Record<string, unknown>) : null;
31
+ const grill = loaded.state.grill && typeof loaded.state.grill === "object" ? (loaded.state.grill as Record<string, unknown>) : null;
32
+ const manifest = scaffoldManifest(outDir, ctx.builtAt, {
33
+ stateFile: loaded.path,
34
+ prdTs: prd && typeof prd["ts"] === "number" ? (prd["ts"] as number) : null,
35
+ grillDone: !!(grill && grill["done"] === true),
36
+ fromSpeedrun: loaded.state.kit?.cursor?.phase === "speedrun",
37
+ });
38
+
39
+ // build.json sits in the selected state file's .thought-layer/ dir.
40
+ const manifestPath = join(dirname(loaded.path), "build.json");
41
+ mkdirSync(dirname(manifestPath), { recursive: true });
42
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
43
+
44
+ const names = Object.keys(files);
45
+ return {
46
+ ok: true,
47
+ message:
48
+ `Scaffolded a deployable static site for "${spec.brandName}" -> ${outAbs} ` +
49
+ `(${names.length} files: ${names.join(", ")}). Manifest: ${manifestPath}. ` +
50
+ `Deploy the publish dir, or build the full product with /tl-build.`,
51
+ details: { publishDir: outDir, outAbs, files: names, manifestPath, brandName: spec.brandName, hadBrand: loaded.state.brand != null },
52
+ };
53
+ } catch (e) {
54
+ return { ok: false, message: `tl_scaffold error: ${(e as Error).message}`, details: {} };
55
+ }
56
+ }
@@ -0,0 +1,276 @@
1
+ // Deterministic, no-LLM starter-site generator: structured spec + brand -> a
2
+ // self-contained, branded, SEO-complete static site. This is the guaranteed
3
+ // "instantly-deployable floor" so even a thin or failed model-build still
4
+ // yields a real, ownable site. The build skill drives the agent to build the
5
+ // PRODUCT; this tool guarantees a deployable landing page from the same spec.
6
+ //
7
+ // Technique ported from the web app's brandLookbookHtml/brandTokens
8
+ // (src/lib/exports.js): esc() HTML escaping, the color-role regex with
9
+ // fallbacks, the fam() Google-Fonts URL builder, inline CSS via :root brand
10
+ // vars. CDATA is needed only inside SVG <style>; HTML <style> does not need it,
11
+ // so this emits plain HTML. Pure: no fs, no Date - the caller supplies builtAt.
12
+
13
+ import type { ProgressState } from "./progress.ts";
14
+
15
+ export interface StarterSiteSpec {
16
+ brandName: string;
17
+ tagline: string;
18
+ pitch: string;
19
+ positioning: string;
20
+ personality: string[];
21
+ palette: { primary: string; accent: string; ink: string; surface: string; muted: string };
22
+ displayFont: string;
23
+ bodyFont: string;
24
+ voiceTone: string;
25
+ logoSvg?: string;
26
+ pricing?: string;
27
+ }
28
+
29
+ export interface ScaffoldOptions {
30
+ domain?: string;
31
+ founderName?: string;
32
+ socialImage?: string;
33
+ }
34
+
35
+ // The shared build manifest the deploy step consumes. Both producers (this
36
+ // scaffold tool and the build skill's agent) write the same shape.
37
+ export interface BuildManifest {
38
+ app: "thought-layer";
39
+ kind: "build";
40
+ version: 1;
41
+ builtAt: string;
42
+ producer: "agent" | "scaffold";
43
+ publishDir: string;
44
+ entry: string;
45
+ stack: string;
46
+ hasBackend: boolean;
47
+ backendNote: string | null;
48
+ buildCommand: string | null;
49
+ installCommand: string | null;
50
+ nodeVersion: string;
51
+ provenance: { stateFile: string; prdTs: number | null; grillDone: boolean; fromSpeedrun: boolean };
52
+ requirements: { total: number; built: number; deferred: number; deferredIds: string[] };
53
+ seo: Record<string, boolean>;
54
+ artifacts: { traceability: string | null; decisions: string | null; seo: string | null };
55
+ verified: { buildRan: boolean; publishDirExists: boolean; entryLoads: boolean; notes: string };
56
+ }
57
+
58
+ const esc = (s: unknown): string =>
59
+ String(s ?? "")
60
+ .replace(/&/g, "&amp;")
61
+ .replace(/</g, "&lt;")
62
+ .replace(/>/g, "&gt;")
63
+ .replace(/"/g, "&quot;");
64
+
65
+ const fam = (f: string): string => f.trim().replace(/\s+/g, "+");
66
+
67
+ // ---- extract a spec from the portable state file -----------------------------
68
+
69
+ export function extractScaffoldSpec(state: ProgressState): StarterSiteSpec {
70
+ const obj = (v: unknown): Record<string, unknown> =>
71
+ v && typeof v === "object" && !Array.isArray(v) ? (v as Record<string, unknown>) : {};
72
+ const str = (v: unknown): string => (typeof v === "string" ? v : "");
73
+
74
+ const brand = obj(state.brand);
75
+ const guide = obj(brand["guide"]);
76
+ const answers = state.answers || {};
77
+
78
+ const palette = Array.isArray(guide["palette"])
79
+ ? (guide["palette"] as Array<{ name?: string; hex?: string }>)
80
+ : [];
81
+ const role = (re: string, fb: string): string => {
82
+ const m = palette.find((p) => p?.name && new RegExp(re, "i").test(p.name));
83
+ return m?.hex || fb;
84
+ };
85
+ const typography = obj(guide["typography"]);
86
+ const fontOf = (slot: unknown, fb: string): string => str(obj(slot)["family"]) || fb;
87
+ const voice = obj(guide["voice"]);
88
+ const logos = Array.isArray(brand["logos"]) ? (brand["logos"] as Array<{ id?: string; svg?: string }>) : [];
89
+ const chosen = logos.find((l) => l?.id === brand["chosenLogoId"]) || logos[0];
90
+
91
+ return {
92
+ brandName: str(guide["brandName"]) || "Your Product",
93
+ tagline: str(guide["tagline"]),
94
+ pitch: str(answers["pitch"]) || str(answers["what-statement"]),
95
+ positioning: str(guide["positioning"]),
96
+ personality: Array.isArray(guide["personality"])
97
+ ? (guide["personality"] as unknown[]).filter((x): x is string => typeof x === "string")
98
+ : [],
99
+ palette: {
100
+ primary: role("primary", palette[0]?.hex || "#1f3a5f"),
101
+ accent: role("accent|secondary", palette[1]?.hex || "#e8743b"),
102
+ ink: role("ink|text|dark|black", "#16202b"),
103
+ surface: role("surface|background|light|paper|off.?white|cream", "#f7f8fa"),
104
+ muted: role("muted|gray|grey|neutral|border", "#8a9099"),
105
+ },
106
+ displayFont: fontOf(typography["display"], "Inter"),
107
+ bodyFont: fontOf(typography["body"], "Inter"),
108
+ voiceTone: str(voice["tone"]),
109
+ logoSvg: chosen?.svg || undefined,
110
+ pricing: str(answers["pricing-model"]) || undefined,
111
+ };
112
+ }
113
+
114
+ // ---- the site -----------------------------------------------------------------
115
+
116
+ function indexHtml(spec: StarterSiteSpec, opts: ScaffoldOptions): string {
117
+ const domain = (opts.domain || "https://example.com").replace(/\/+$/, "");
118
+ const founder = opts.founderName || "";
119
+ const social = opts.socialImage || `${domain}/og-image.png`;
120
+ const name = esc(spec.brandName);
121
+ const headline = esc(spec.tagline || spec.brandName);
122
+ const lead = esc(spec.pitch || spec.positioning);
123
+ const desc = esc(spec.pitch || spec.positioning || spec.brandName);
124
+ const fonts = `https://fonts.googleapis.com/css2?family=${fam(spec.displayFont)}:wght@400;600;700;800&family=${fam(spec.bodyFont)}:wght@400;500;600&display=swap`;
125
+
126
+ const graph: Array<Record<string, unknown>> = [
127
+ { "@type": "Organization", "@id": `${domain}/#org`, name: spec.brandName, url: `${domain}/`, ...(founder ? { founder: { "@id": `${domain}/#founder` } } : {}) },
128
+ ...(founder ? [{ "@type": "Person", "@id": `${domain}/#founder`, name: founder, worksFor: { "@id": `${domain}/#org` } }] : []),
129
+ { "@type": "WebSite", "@id": `${domain}/#website`, url: `${domain}/`, name: spec.brandName, publisher: { "@id": `${domain}/#org` } },
130
+ { "@type": "WebPage", "@id": `${domain}/#webpage`, url: `${domain}/`, name: `${spec.brandName}${spec.tagline ? " - " + spec.tagline : ""}`, isPartOf: { "@id": `${domain}/#website` }, about: { "@id": `${domain}/#org` }, description: spec.pitch || spec.positioning || spec.brandName },
131
+ ];
132
+ // JSON.stringify does NOT escape < or >, so a value containing "</script>"
133
+ // would break out of the ld+json <script> block. Unicode-escape both.
134
+ const jsonLd = JSON.stringify({ "@context": "https://schema.org", "@graph": graph })
135
+ .replace(/</g, "\\u003c")
136
+ .replace(/>/g, "\\u003e");
137
+
138
+ const lockup = spec.logoSvg ? spec.logoSvg : `<span class="wordmark">${name}</span>`;
139
+ const pills = spec.personality.map((t) => `<li class="pill">${esc(t)}</li>`).join("");
140
+ const positioningSection = spec.positioning
141
+ ? `<section class="value" aria-labelledby="value-h"><div class="wrap"><h2 id="value-h">Who it is for</h2><p>${esc(spec.positioning)}</p>${pills ? `<ul class="pills" aria-label="What it stands for">${pills}</ul>` : ""}</div></section>`
142
+ : "";
143
+ const pricingSection = spec.pricing
144
+ ? `<section class="pricing" aria-labelledby="pricing-h"><div class="wrap"><h2 id="pricing-h">Pricing</h2><p class="price">${esc(spec.pricing)}</p></div></section>`
145
+ : "";
146
+
147
+ return `<!DOCTYPE html>
148
+ <html lang="en">
149
+ <head>
150
+ <meta charset="UTF-8">
151
+ <meta name="viewport" content="width=device-width, initial-scale=1">
152
+ <title>${name}${spec.tagline ? " - " + headline : ""}</title>
153
+ <meta name="description" content="${desc}">
154
+ <link rel="canonical" href="${domain}/">
155
+ <meta property="og:type" content="website">
156
+ <meta property="og:title" content="${name}">
157
+ <meta property="og:description" content="${desc}">
158
+ <meta property="og:url" content="${domain}/">
159
+ <meta property="og:image" content="${esc(social)}">
160
+ <meta name="twitter:card" content="summary_large_image">
161
+ <meta name="twitter:title" content="${name}">
162
+ <meta name="twitter:description" content="${desc}">
163
+ <meta name="twitter:image" content="${esc(social)}">
164
+ <link href="${esc(fonts)}" rel="stylesheet">
165
+ <script type="application/ld+json">${jsonLd}</script>
166
+ <style>
167
+ :root{--p:${spec.palette.primary};--a:${spec.palette.accent};--ink:${spec.palette.ink};--su:${spec.palette.surface};--mu:${spec.palette.muted};--disp:'${spec.displayFont}',system-ui,sans-serif;--body:'${spec.bodyFont}',system-ui,sans-serif}
168
+ *{margin:0;padding:0;box-sizing:border-box}
169
+ html{scroll-behavior:smooth}
170
+ body{background:var(--su);color:var(--ink);font-family:var(--body);line-height:1.6;-webkit-font-smoothing:antialiased}
171
+ .wrap{max-width:960px;margin:0 auto;padding:0 24px}
172
+ a{color:var(--a)}
173
+ :focus-visible{outline:3px solid var(--a);outline-offset:2px}
174
+ .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}
175
+ header{padding:24px 0}
176
+ header .wrap{display:flex;align-items:center;justify-content:space-between;gap:16px}
177
+ .wordmark{font-family:var(--disp);font-weight:800;font-size:22px;color:var(--p)}
178
+ .logo svg,.logo img{height:36px;width:auto}
179
+ .cta{display:inline-block;background:var(--p);color:#fff;text-decoration:none;font-weight:600;padding:12px 22px;border-radius:10px;border:0;cursor:pointer;font-size:15px}
180
+ .cta.alt{background:var(--a)}
181
+ .hero{padding:72px 0 56px}
182
+ .hero h1{font-family:var(--disp);font-weight:800;font-size:clamp(34px,6vw,60px);line-height:1.05;color:var(--ink);letter-spacing:-0.02em;max-width:14ch}
183
+ .hero p.lead{font-size:clamp(17px,2.4vw,22px);opacity:.82;margin:20px 0 28px;max-width:42ch}
184
+ .signup{display:flex;gap:10px;flex-wrap:wrap;max-width:460px}
185
+ .signup input[type=email]{flex:1 1 220px;padding:12px 14px;border:1px solid var(--mu);border-radius:10px;font:inherit;background:#fff;color:var(--ink)}
186
+ .value,.pricing{padding:48px 0;border-top:1px solid color-mix(in srgb,var(--mu) 40%,transparent)}
187
+ .value h2,.pricing h2{font-family:var(--disp);font-weight:700;font-size:28px;margin-bottom:12px}
188
+ .value p{max-width:54ch;opacity:.85}
189
+ ul.pills{list-style:none;display:flex;flex-wrap:wrap;gap:10px;margin-top:22px}
190
+ .pill{font-size:13px;font-weight:600;color:var(--p);background:color-mix(in srgb,var(--p) 12%,transparent);padding:6px 14px;border-radius:999px}
191
+ .pricing .price{font-family:var(--disp);font-size:24px;font-weight:700;color:var(--p);margin-top:8px}
192
+ footer{padding:48px 0;border-top:1px solid color-mix(in srgb,var(--mu) 40%,transparent);color:var(--mu);font-size:13px}
193
+ @media (max-width:640px){.hero{padding:48px 0 40px}header .wrap{flex-direction:column;align-items:flex-start}}
194
+ </style>
195
+ </head>
196
+ <body>
197
+ <header><div class="wrap"><div class="logo">${lockup}</div><a class="cta" href="#get-started">Get early access</a></div></header>
198
+ <main>
199
+ <section class="hero" aria-labelledby="hero-h"><div class="wrap">
200
+ <h1 id="hero-h">${headline}</h1>
201
+ <p class="lead">${lead}</p>
202
+ <form name="signups" id="get-started" method="POST" data-netlify="true" class="signup">
203
+ <input type="hidden" name="form-name" value="signups">
204
+ <label class="sr-only" for="email">Email address</label>
205
+ <input id="email" type="email" name="email" placeholder="you@email.com" required>
206
+ <button type="submit" class="cta alt">Get early access</button>
207
+ </form>
208
+ </div></section>
209
+ ${positioningSection}
210
+ ${pricingSection}
211
+ </main>
212
+ <footer><div class="wrap"><p>${name}${founder ? ` - by ${esc(founder)}` : ""}. Scaffolded by The Thought Layer.</p></div></footer>
213
+ </body>
214
+ </html>
215
+ `;
216
+ }
217
+
218
+ function companionFiles(spec: StarterSiteSpec, opts: ScaffoldOptions): Record<string, string> {
219
+ const domain = (opts.domain || "https://example.com").replace(/\/+$/, "");
220
+ const name = spec.brandName;
221
+ const summary = spec.pitch || spec.positioning || name;
222
+
223
+ const llms = `# ${name}\n\n> ${summary}\n\n## About\n${name}${spec.tagline ? ` - ${spec.tagline}` : ""}. ${spec.positioning || summary}\n\n## Pages\n- [Home](${domain}/) - ${spec.tagline || summary}\n\n## FAQ\n- What is ${name}? ${summary}\n- Who is it for? ${spec.positioning || "See the home page."}\n`;
224
+
225
+ const robots = `User-agent: *\nAllow: /\n\nUser-agent: GPTBot\nAllow: /\n\nUser-agent: ClaudeBot\nAllow: /\n\nUser-agent: PerplexityBot\nAllow: /\n\nUser-agent: Google-Extended\nAllow: /\n\nSitemap: ${domain}/sitemap.xml\n`;
226
+
227
+ const sitemap = `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n <url><loc>${domain}/</loc><changefreq>weekly</changefreq><priority>1.0</priority></url>\n</urlset>\n`;
228
+
229
+ const redirects = `/* /index.html 200\n`;
230
+
231
+ const netlifyToml = `[build]\n publish = "."\n\n[[redirects]]\n from = "/*"\n to = "/index.html"\n status = 200\n`;
232
+
233
+ const seoDoc = `# SEO and deployment\n\nThis static site was scaffolded deterministically by The Thought Layer. It is ready to deploy (drag the publish folder onto https://app.netlify.com/drop, or use the deploy step). Everything below is already in place; the lines marked TO FILL need your input.\n\n## In place\n- **Structured data**: schema.org JSON-LD (@graph) with Organization, WebSite, and WebPage in index.html.\n- **/llms.txt**: an AI-crawler summary at the site root.\n- **robots.txt**: allows search + AI crawlers and points to the sitemap.\n- **sitemap.xml**: lists the home page.\n- **Canonical + Open Graph + Twitter Card** meta on the page.\n- **_redirects** and **netlify.toml**: SPA fallback (/* -> /index.html).\n- **Email capture**: a Netlify Forms signup ("signups") - submissions appear in your Netlify dashboard after deploy, no backend needed.\n- Semantic, accessible HTML (landmarks, heading order, labelled form, visible focus).\n\n## TO FILL\n- **Domain**: replace ${domain} with your real domain in index.html (canonical/OG), llms.txt, robots.txt, sitemap.xml. Pass --domain to the scaffold tool to set it up front.\n- **Founder + sameAs**: add a Person with real sameAs profile links (LinkedIn, X) to the JSON-LD, and cross-reference the Organization. Pass --founder to seed the name.\n- **Social image**: add a 1200x630 image at /og-image.png (referenced by og:image / twitter:image).\n- **The product**: this is a landing/coming-soon page. Build the actual product with the thought-layer-build skill (/tl-build).\n`;
234
+
235
+ return {
236
+ "llms.txt": llms,
237
+ "robots.txt": robots,
238
+ "sitemap.xml": sitemap,
239
+ "_redirects": redirects,
240
+ "netlify.toml": netlifyToml,
241
+ "SEO.md": seoDoc,
242
+ };
243
+ }
244
+
245
+ export function buildStarterSite(spec: StarterSiteSpec, opts: ScaffoldOptions = {}): { files: Record<string, string> } {
246
+ return { files: { "index.html": indexHtml(spec, opts), ...companionFiles(spec, opts) } };
247
+ }
248
+
249
+ // ---- the manifest (scaffold producer) ----------------------------------------
250
+
251
+ export function scaffoldManifest(
252
+ publishDir: string,
253
+ builtAt: string,
254
+ provenance: { stateFile: string; prdTs: number | null; grillDone: boolean; fromSpeedrun: boolean },
255
+ ): BuildManifest {
256
+ return {
257
+ app: "thought-layer",
258
+ kind: "build",
259
+ version: 1,
260
+ builtAt,
261
+ producer: "scaffold",
262
+ publishDir,
263
+ entry: "index.html",
264
+ stack: "static",
265
+ hasBackend: false,
266
+ backendNote: null,
267
+ buildCommand: null,
268
+ installCommand: null,
269
+ nodeVersion: "20",
270
+ provenance,
271
+ requirements: { total: 0, built: 0, deferred: 0, deferredIds: [] },
272
+ seo: { jsonLd: true, llmsTxt: true, sitemap: true, robots: true, canonical: true, openGraph: true, socialImage: false, semanticHtml: true, seoDoc: true, netlifyToml: true },
273
+ artifacts: { traceability: null, decisions: null, seo: "SEO.md" },
274
+ verified: { buildRan: true, publishDirExists: true, entryLoads: true, notes: "deterministic static scaffold; landing page + SEO files written" },
275
+ };
276
+ }
package/dist/tl.js CHANGED
@@ -397,11 +397,284 @@ Pick one with --path .thought-layer/<name>.json, or set ${STATE_ENV} for the ses
397
397
  }
398
398
  }
399
399
 
400
+ // core/scaffold-io.ts
401
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
402
+ import { dirname as dirname2, isAbsolute as isAbsolute2, join as join2, resolve as resolve2 } from "path";
403
+
404
+ // core/scaffold.ts
405
+ var esc = (s) => String(s ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
406
+ var fam = (f) => f.trim().replace(/\s+/g, "+");
407
+ function extractScaffoldSpec(state) {
408
+ const obj = (v) => v && typeof v === "object" && !Array.isArray(v) ? v : {};
409
+ const str = (v) => typeof v === "string" ? v : "";
410
+ const brand = obj(state.brand);
411
+ const guide = obj(brand["guide"]);
412
+ const answers = state.answers || {};
413
+ const palette = Array.isArray(guide["palette"]) ? guide["palette"] : [];
414
+ const role = (re, fb) => {
415
+ const m = palette.find((p) => p?.name && new RegExp(re, "i").test(p.name));
416
+ return m?.hex || fb;
417
+ };
418
+ const typography = obj(guide["typography"]);
419
+ const fontOf = (slot, fb) => str(obj(slot)["family"]) || fb;
420
+ const voice = obj(guide["voice"]);
421
+ const logos = Array.isArray(brand["logos"]) ? brand["logos"] : [];
422
+ const chosen = logos.find((l) => l?.id === brand["chosenLogoId"]) || logos[0];
423
+ return {
424
+ brandName: str(guide["brandName"]) || "Your Product",
425
+ tagline: str(guide["tagline"]),
426
+ pitch: str(answers["pitch"]) || str(answers["what-statement"]),
427
+ positioning: str(guide["positioning"]),
428
+ personality: Array.isArray(guide["personality"]) ? guide["personality"].filter((x) => typeof x === "string") : [],
429
+ palette: {
430
+ primary: role("primary", palette[0]?.hex || "#1f3a5f"),
431
+ accent: role("accent|secondary", palette[1]?.hex || "#e8743b"),
432
+ ink: role("ink|text|dark|black", "#16202b"),
433
+ surface: role("surface|background|light|paper|off.?white|cream", "#f7f8fa"),
434
+ muted: role("muted|gray|grey|neutral|border", "#8a9099")
435
+ },
436
+ displayFont: fontOf(typography["display"], "Inter"),
437
+ bodyFont: fontOf(typography["body"], "Inter"),
438
+ voiceTone: str(voice["tone"]),
439
+ logoSvg: chosen?.svg || void 0,
440
+ pricing: str(answers["pricing-model"]) || void 0
441
+ };
442
+ }
443
+ function indexHtml(spec, opts) {
444
+ const domain = (opts.domain || "https://example.com").replace(/\/+$/, "");
445
+ const founder = opts.founderName || "";
446
+ const social = opts.socialImage || `${domain}/og-image.png`;
447
+ const name = esc(spec.brandName);
448
+ const headline = esc(spec.tagline || spec.brandName);
449
+ const lead = esc(spec.pitch || spec.positioning);
450
+ const desc = esc(spec.pitch || spec.positioning || spec.brandName);
451
+ const fonts = `https://fonts.googleapis.com/css2?family=${fam(spec.displayFont)}:wght@400;600;700;800&family=${fam(spec.bodyFont)}:wght@400;500;600&display=swap`;
452
+ const graph = [
453
+ { "@type": "Organization", "@id": `${domain}/#org`, name: spec.brandName, url: `${domain}/`, ...founder ? { founder: { "@id": `${domain}/#founder` } } : {} },
454
+ ...founder ? [{ "@type": "Person", "@id": `${domain}/#founder`, name: founder, worksFor: { "@id": `${domain}/#org` } }] : [],
455
+ { "@type": "WebSite", "@id": `${domain}/#website`, url: `${domain}/`, name: spec.brandName, publisher: { "@id": `${domain}/#org` } },
456
+ { "@type": "WebPage", "@id": `${domain}/#webpage`, url: `${domain}/`, name: `${spec.brandName}${spec.tagline ? " - " + spec.tagline : ""}`, isPartOf: { "@id": `${domain}/#website` }, about: { "@id": `${domain}/#org` }, description: spec.pitch || spec.positioning || spec.brandName }
457
+ ];
458
+ const jsonLd = JSON.stringify({ "@context": "https://schema.org", "@graph": graph }).replace(/</g, "\\u003c").replace(/>/g, "\\u003e");
459
+ const lockup = spec.logoSvg ? spec.logoSvg : `<span class="wordmark">${name}</span>`;
460
+ const pills = spec.personality.map((t) => `<li class="pill">${esc(t)}</li>`).join("");
461
+ const positioningSection = spec.positioning ? `<section class="value" aria-labelledby="value-h"><div class="wrap"><h2 id="value-h">Who it is for</h2><p>${esc(spec.positioning)}</p>${pills ? `<ul class="pills" aria-label="What it stands for">${pills}</ul>` : ""}</div></section>` : "";
462
+ const pricingSection = spec.pricing ? `<section class="pricing" aria-labelledby="pricing-h"><div class="wrap"><h2 id="pricing-h">Pricing</h2><p class="price">${esc(spec.pricing)}</p></div></section>` : "";
463
+ return `<!DOCTYPE html>
464
+ <html lang="en">
465
+ <head>
466
+ <meta charset="UTF-8">
467
+ <meta name="viewport" content="width=device-width, initial-scale=1">
468
+ <title>${name}${spec.tagline ? " - " + headline : ""}</title>
469
+ <meta name="description" content="${desc}">
470
+ <link rel="canonical" href="${domain}/">
471
+ <meta property="og:type" content="website">
472
+ <meta property="og:title" content="${name}">
473
+ <meta property="og:description" content="${desc}">
474
+ <meta property="og:url" content="${domain}/">
475
+ <meta property="og:image" content="${esc(social)}">
476
+ <meta name="twitter:card" content="summary_large_image">
477
+ <meta name="twitter:title" content="${name}">
478
+ <meta name="twitter:description" content="${desc}">
479
+ <meta name="twitter:image" content="${esc(social)}">
480
+ <link href="${esc(fonts)}" rel="stylesheet">
481
+ <script type="application/ld+json">${jsonLd}</script>
482
+ <style>
483
+ :root{--p:${spec.palette.primary};--a:${spec.palette.accent};--ink:${spec.palette.ink};--su:${spec.palette.surface};--mu:${spec.palette.muted};--disp:'${spec.displayFont}',system-ui,sans-serif;--body:'${spec.bodyFont}',system-ui,sans-serif}
484
+ *{margin:0;padding:0;box-sizing:border-box}
485
+ html{scroll-behavior:smooth}
486
+ body{background:var(--su);color:var(--ink);font-family:var(--body);line-height:1.6;-webkit-font-smoothing:antialiased}
487
+ .wrap{max-width:960px;margin:0 auto;padding:0 24px}
488
+ a{color:var(--a)}
489
+ :focus-visible{outline:3px solid var(--a);outline-offset:2px}
490
+ .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}
491
+ header{padding:24px 0}
492
+ header .wrap{display:flex;align-items:center;justify-content:space-between;gap:16px}
493
+ .wordmark{font-family:var(--disp);font-weight:800;font-size:22px;color:var(--p)}
494
+ .logo svg,.logo img{height:36px;width:auto}
495
+ .cta{display:inline-block;background:var(--p);color:#fff;text-decoration:none;font-weight:600;padding:12px 22px;border-radius:10px;border:0;cursor:pointer;font-size:15px}
496
+ .cta.alt{background:var(--a)}
497
+ .hero{padding:72px 0 56px}
498
+ .hero h1{font-family:var(--disp);font-weight:800;font-size:clamp(34px,6vw,60px);line-height:1.05;color:var(--ink);letter-spacing:-0.02em;max-width:14ch}
499
+ .hero p.lead{font-size:clamp(17px,2.4vw,22px);opacity:.82;margin:20px 0 28px;max-width:42ch}
500
+ .signup{display:flex;gap:10px;flex-wrap:wrap;max-width:460px}
501
+ .signup input[type=email]{flex:1 1 220px;padding:12px 14px;border:1px solid var(--mu);border-radius:10px;font:inherit;background:#fff;color:var(--ink)}
502
+ .value,.pricing{padding:48px 0;border-top:1px solid color-mix(in srgb,var(--mu) 40%,transparent)}
503
+ .value h2,.pricing h2{font-family:var(--disp);font-weight:700;font-size:28px;margin-bottom:12px}
504
+ .value p{max-width:54ch;opacity:.85}
505
+ ul.pills{list-style:none;display:flex;flex-wrap:wrap;gap:10px;margin-top:22px}
506
+ .pill{font-size:13px;font-weight:600;color:var(--p);background:color-mix(in srgb,var(--p) 12%,transparent);padding:6px 14px;border-radius:999px}
507
+ .pricing .price{font-family:var(--disp);font-size:24px;font-weight:700;color:var(--p);margin-top:8px}
508
+ footer{padding:48px 0;border-top:1px solid color-mix(in srgb,var(--mu) 40%,transparent);color:var(--mu);font-size:13px}
509
+ @media (max-width:640px){.hero{padding:48px 0 40px}header .wrap{flex-direction:column;align-items:flex-start}}
510
+ </style>
511
+ </head>
512
+ <body>
513
+ <header><div class="wrap"><div class="logo">${lockup}</div><a class="cta" href="#get-started">Get early access</a></div></header>
514
+ <main>
515
+ <section class="hero" aria-labelledby="hero-h"><div class="wrap">
516
+ <h1 id="hero-h">${headline}</h1>
517
+ <p class="lead">${lead}</p>
518
+ <form name="signups" id="get-started" method="POST" data-netlify="true" class="signup">
519
+ <input type="hidden" name="form-name" value="signups">
520
+ <label class="sr-only" for="email">Email address</label>
521
+ <input id="email" type="email" name="email" placeholder="you@email.com" required>
522
+ <button type="submit" class="cta alt">Get early access</button>
523
+ </form>
524
+ </div></section>
525
+ ${positioningSection}
526
+ ${pricingSection}
527
+ </main>
528
+ <footer><div class="wrap"><p>${name}${founder ? ` - by ${esc(founder)}` : ""}. Scaffolded by The Thought Layer.</p></div></footer>
529
+ </body>
530
+ </html>
531
+ `;
532
+ }
533
+ function companionFiles(spec, opts) {
534
+ const domain = (opts.domain || "https://example.com").replace(/\/+$/, "");
535
+ const name = spec.brandName;
536
+ const summary = spec.pitch || spec.positioning || name;
537
+ const llms = `# ${name}
538
+
539
+ > ${summary}
540
+
541
+ ## About
542
+ ${name}${spec.tagline ? ` - ${spec.tagline}` : ""}. ${spec.positioning || summary}
543
+
544
+ ## Pages
545
+ - [Home](${domain}/) - ${spec.tagline || summary}
546
+
547
+ ## FAQ
548
+ - What is ${name}? ${summary}
549
+ - Who is it for? ${spec.positioning || "See the home page."}
550
+ `;
551
+ const robots = `User-agent: *
552
+ Allow: /
553
+
554
+ User-agent: GPTBot
555
+ Allow: /
556
+
557
+ User-agent: ClaudeBot
558
+ Allow: /
559
+
560
+ User-agent: PerplexityBot
561
+ Allow: /
562
+
563
+ User-agent: Google-Extended
564
+ Allow: /
565
+
566
+ Sitemap: ${domain}/sitemap.xml
567
+ `;
568
+ const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
569
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
570
+ <url><loc>${domain}/</loc><changefreq>weekly</changefreq><priority>1.0</priority></url>
571
+ </urlset>
572
+ `;
573
+ const redirects = `/* /index.html 200
574
+ `;
575
+ const netlifyToml = `[build]
576
+ publish = "."
577
+
578
+ [[redirects]]
579
+ from = "/*"
580
+ to = "/index.html"
581
+ status = 200
582
+ `;
583
+ const seoDoc = `# SEO and deployment
584
+
585
+ This static site was scaffolded deterministically by The Thought Layer. It is ready to deploy (drag the publish folder onto https://app.netlify.com/drop, or use the deploy step). Everything below is already in place; the lines marked TO FILL need your input.
586
+
587
+ ## In place
588
+ - **Structured data**: schema.org JSON-LD (@graph) with Organization, WebSite, and WebPage in index.html.
589
+ - **/llms.txt**: an AI-crawler summary at the site root.
590
+ - **robots.txt**: allows search + AI crawlers and points to the sitemap.
591
+ - **sitemap.xml**: lists the home page.
592
+ - **Canonical + Open Graph + Twitter Card** meta on the page.
593
+ - **_redirects** and **netlify.toml**: SPA fallback (/* -> /index.html).
594
+ - **Email capture**: a Netlify Forms signup ("signups") - submissions appear in your Netlify dashboard after deploy, no backend needed.
595
+ - Semantic, accessible HTML (landmarks, heading order, labelled form, visible focus).
596
+
597
+ ## TO FILL
598
+ - **Domain**: replace ${domain} with your real domain in index.html (canonical/OG), llms.txt, robots.txt, sitemap.xml. Pass --domain to the scaffold tool to set it up front.
599
+ - **Founder + sameAs**: add a Person with real sameAs profile links (LinkedIn, X) to the JSON-LD, and cross-reference the Organization. Pass --founder to seed the name.
600
+ - **Social image**: add a 1200x630 image at /og-image.png (referenced by og:image / twitter:image).
601
+ - **The product**: this is a landing/coming-soon page. Build the actual product with the thought-layer-build skill (/tl-build).
602
+ `;
603
+ return {
604
+ "llms.txt": llms,
605
+ "robots.txt": robots,
606
+ "sitemap.xml": sitemap,
607
+ "_redirects": redirects,
608
+ "netlify.toml": netlifyToml,
609
+ "SEO.md": seoDoc
610
+ };
611
+ }
612
+ function buildStarterSite(spec, opts = {}) {
613
+ return { files: { "index.html": indexHtml(spec, opts), ...companionFiles(spec, opts) } };
614
+ }
615
+ function scaffoldManifest(publishDir, builtAt, provenance) {
616
+ return {
617
+ app: "thought-layer",
618
+ kind: "build",
619
+ version: 1,
620
+ builtAt,
621
+ producer: "scaffold",
622
+ publishDir,
623
+ entry: "index.html",
624
+ stack: "static",
625
+ hasBackend: false,
626
+ backendNote: null,
627
+ buildCommand: null,
628
+ installCommand: null,
629
+ nodeVersion: "20",
630
+ provenance,
631
+ requirements: { total: 0, built: 0, deferred: 0, deferredIds: [] },
632
+ seo: { jsonLd: true, llmsTxt: true, sitemap: true, robots: true, canonical: true, openGraph: true, socialImage: false, semanticHtml: true, seoDoc: true, netlifyToml: true },
633
+ artifacts: { traceability: null, decisions: null, seo: "SEO.md" },
634
+ verified: { buildRan: true, publishDirExists: true, entryLoads: true, notes: "deterministic static scaffold; landing page + SEO files written" }
635
+ };
636
+ }
637
+
638
+ // core/scaffold-io.ts
639
+ function runScaffold(opts, ctx) {
640
+ try {
641
+ const loaded = loadStateFile(opts.path);
642
+ const spec = extractScaffoldSpec(loaded.state);
643
+ const { files } = buildStarterSite(spec, { domain: opts.domain, founderName: opts.founderName, socialImage: opts.socialImage });
644
+ const outDir = opts.outDir || "dist";
645
+ const outAbs = isAbsolute2(outDir) ? outDir : resolve2(process.cwd(), outDir);
646
+ mkdirSync2(outAbs, { recursive: true });
647
+ for (const [name, content] of Object.entries(files)) {
648
+ writeFileSync2(join2(outAbs, name), content);
649
+ }
650
+ const prd = loaded.state.prd && typeof loaded.state.prd === "object" ? loaded.state.prd : null;
651
+ const grill = loaded.state.grill && typeof loaded.state.grill === "object" ? loaded.state.grill : null;
652
+ const manifest = scaffoldManifest(outDir, ctx.builtAt, {
653
+ stateFile: loaded.path,
654
+ prdTs: prd && typeof prd["ts"] === "number" ? prd["ts"] : null,
655
+ grillDone: !!(grill && grill["done"] === true),
656
+ fromSpeedrun: loaded.state.kit?.cursor?.phase === "speedrun"
657
+ });
658
+ const manifestPath = join2(dirname2(loaded.path), "build.json");
659
+ mkdirSync2(dirname2(manifestPath), { recursive: true });
660
+ writeFileSync2(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
661
+ const names = Object.keys(files);
662
+ return {
663
+ ok: true,
664
+ message: `Scaffolded a deployable static site for "${spec.brandName}" -> ${outAbs} (${names.length} files: ${names.join(", ")}). Manifest: ${manifestPath}. Deploy the publish dir, or build the full product with /tl-build.`,
665
+ details: { publishDir: outDir, outAbs, files: names, manifestPath, brandName: spec.brandName, hadBrand: loaded.state.brand != null }
666
+ };
667
+ } catch (e) {
668
+ return { ok: false, message: `tl_scaffold error: ${e.message}`, details: {} };
669
+ }
670
+ }
671
+
400
672
  // bin/tl.ts
401
673
  var HELP = `tl - read/write a portable Thought Layer state file (default: .thought-layer/state.json)
402
674
 
403
675
  tl read [path] [--json] where the run stands
404
676
  tl list [dir] list the state files under .thought-layer/ (juggle several ideas)
677
+ tl scaffold [--out dist] [--domain x.com] [--founder "Name"] deterministic deployable static site from the spec + brand
405
678
  tl export [path] handoff check
406
679
  tl answer <qId> <value> [path] record an answer
407
680
  tl feedback --data '<json>' record a panel verdict ({qId,mode,personas,endState,round})
@@ -476,6 +749,20 @@ function main() {
476
749
  console.log(HELP);
477
750
  process.exit(0);
478
751
  }
752
+ if (args[0] === "scaffold") {
753
+ const r2 = runScaffold(
754
+ {
755
+ path: typeof flags["path"] === "string" ? flags["path"] : void 0,
756
+ outDir: typeof flags["out"] === "string" ? flags["out"] : void 0,
757
+ domain: typeof flags["domain"] === "string" ? flags["domain"] : void 0,
758
+ founderName: typeof flags["founder"] === "string" ? flags["founder"] : void 0
759
+ },
760
+ { builtAt: (/* @__PURE__ */ new Date()).toISOString() }
761
+ );
762
+ if (flags["json"]) console.log(JSON.stringify(r2.details, null, 2));
763
+ else console.log(r2.message);
764
+ process.exit(r2.ok ? 0 : 1);
765
+ }
479
766
  let payload;
480
767
  try {
481
768
  payload = buildOp(args, flags);
@@ -9,7 +9,7 @@ import {
9
9
  aggregateConfidence, statusFromConfidence, gradeFromConfidence,
10
10
  checkDomains, registrarSearchUrl,
11
11
  computeProjection, fmtMoney,
12
- applyStateOp,
12
+ applyStateOp, runScaffold,
13
13
  type Assumptions, type StateOp,
14
14
  } from "../core/index.ts";
15
15
 
@@ -179,4 +179,28 @@ export default function (pi: ExtensionAPI) {
179
179
  return text(r.message, r.details);
180
180
  },
181
181
  });
182
+
183
+ // tl_scaffold: deterministically generate a self-contained, branded,
184
+ // SEO-complete static site (the instantly-deployable floor) from the spec +
185
+ // brand in the state file - no model needed. Writes the site to a publish dir
186
+ // plus a build.json manifest the deploy step consumes.
187
+ pi.registerTool({
188
+ name: "tl_scaffold",
189
+ label: "Thought Layer: scaffold",
190
+ description:
191
+ "Deterministically scaffold a self-contained, branded, SEO-complete static landing site (an instantly-deployable floor) from the spec + brand in the state file - no model call. " +
192
+ "Use it for the fastest path to something live, or as the floor when a model build is thin or fails. Writes the site (index.html + llms.txt/robots.txt/sitemap.xml/_redirects/netlify.toml/SEO.md) to the publish dir and a build.json manifest for the deploy step. " +
193
+ "The full product is built by the thought-layer-build skill; this is the guaranteed deployable baseline.",
194
+ parameters: Type.Object({
195
+ path: Type.Optional(Type.String({ description: "State file (or project dir) to read the spec + brand from. Defaults to ./.thought-layer/state.json; honors a named file." })),
196
+ outDir: Type.Optional(Type.String({ description: "Publish directory to write the site into. Defaults to ./dist." })),
197
+ domain: Type.Optional(Type.String({ description: "The site's real domain (e.g. https://acme.com) for canonical/OG/sitemap. Defaults to a placeholder you fill later." })),
198
+ founder: Type.Optional(Type.String({ description: "Founder name for the schema.org Person + footer. Optional." })),
199
+ }),
200
+ async execute(_id, params): Promise<ToolResult> {
201
+ const p = params as { path?: string; outDir?: string; domain?: string; founder?: string };
202
+ const r = runScaffold({ path: p.path, outDir: p.outDir, domain: p.domain, founderName: p.founder }, { builtAt: new Date().toISOString() });
203
+ return text(r.message, r.details);
204
+ },
205
+ });
182
206
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hobocode/thought-layer",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "description": "The Thought Layer: rigor for building. Validate an idea, grill it into a buildable spec, then build and deploy it, inside the agent you already use. BYOK, no telemetry.",
5
5
  "license": "MIT",
6
6
  "author": "Hobocode LLC <jerm@hobocode.net>",
@@ -64,6 +64,8 @@
64
64
  "core/stage-map.ts",
65
65
  "core/state-file.ts",
66
66
  "core/state-ops.ts",
67
+ "core/scaffold.ts",
68
+ "core/scaffold-io.ts",
67
69
  "dist",
68
70
  "README.md",
69
71
  "LICENSE"
@@ -0,0 +1,7 @@
1
+ Apply the **thought-layer-build** skill. Build the hardened PRD into a static-first, deploy-ready artifact - a self-contained site or a Vite/static build that yields a predictable `dist/`-style publish directory.
2
+
3
+ If there is no hardened PRD in the state file (`state.prd.markdown` + requirements), say so and point me to `/tl` (or `/tl-prd` then `/tl-grill`) rather than building from a bare idea - only proceed cold if I tell you to.
4
+
5
+ Honor the ubiquitous language (the glossary), R-ID traceability, the out-of-scope list, mobile+desktop, the brand if present, and the full SEO/discoverability layer. Default to static; escalate to a backend only when a requirement genuinely needs one, and flag loudly that the default deploy path is static. Verify the build runs, and leave `.thought-layer/build.json` + `DECISIONS.md` + `TRACEABILITY.md`.
6
+
7
+ Read the spec from the state file (default `.thought-layer/state.json`; honor `--path` / `THOUGHT_LAYER_STATE` if a named file is in use). For the fastest deployable floor, or if a full build is too much, run the **tl_scaffold** tool to write a branded, SEO-complete static landing site and the same `build.json` manifest.
@@ -0,0 +1,98 @@
1
+ ---
2
+ name: thought-layer-build
3
+ description: "Turn the hardened PRD into a static-first, deploy-ready artifact, built directly by this agent. Reads the build brief from the shared state file (PRD, glossary, R-ID requirements, brand, business context, open to-dos), then builds a self-contained site or a Vite/static build that yields a predictable publish directory (dist/), honoring ubiquitous language, R-ID traceability, the out-of-scope list, mobile+desktop, brand, and the full SEO/discoverability layer. Escalates to a backend only when the spec genuinely requires server-side, and flags loudly that the default deploy path is static. Verifies the build runs and writes a .thought-layer/build.json manifest for the deploy step. Run it after the grill has hardened the PRD; it is a standalone build step, not a validation stage."
4
+ ---
5
+
6
+ # Build it: the hardened PRD becomes a deploy-ready artifact
7
+
8
+ You are not generating a prompt to paste elsewhere. **You are the agent that builds.** The hardened PRD is your brief; build it now, static-first, and verify it actually runs. AI made building cheap, so the value is building the *right* thing, honestly, and shipping something real and ownable. One pass, then verify. This is a build step, not a second framework: no panel, no stages, no per-turn loop.
9
+
10
+ For the fastest possible deployable thing, or as a floor when a full build is too much, you can run the **tl_scaffold** tool first (or instead) - it deterministically writes a branded, SEO-complete static landing site you can ship immediately, then build the real product on top.
11
+
12
+ ## Precondition: a hardened PRD must exist
13
+
14
+ Read the state file first (next section). Then:
15
+ - **Required:** `state.prd` with a non-empty `markdown` and a non-empty `requirements` array.
16
+ - **Ideal:** `state.grill.done === true` (the PRD was actually hardened). If `prd` exists but the grill is absent or not done, build anyway and warn in one line: "Building from a drafted-but-not-grilled PRD - gaps the grill would have caught may surface; `/tl-grill` first would tighten it."
17
+ - **Refuse** only when there is no `prd.markdown` / no requirements at all: say so and point to `/tl` (the full framework) or at least `/tl-prd` then `/tl-grill`. Proceed cold only if the user explicitly says to, and record in DECISIONS.md that the spec was incomplete.
18
+
19
+ ## Read the brief from the state file
20
+
21
+ The spec lives in `.thought-layer/state.json` (or a named file). Honor the same selection as the rest of the kit: an explicit `--path` / tool `path` wins, then `THOUGHT_LAYER_STATE`, then the default. If the `tl_state` tool is available (Pi), `tl_state read`; otherwise `tl read --json` (the CLI). If neither a path nor the env is set, `tl list` (or `tl_state list`) first and **ask which idea to build** when several exist, then stick to that path for the whole session and write `build.json` next to it.
22
+
23
+ Assemble the brief from the state:
24
+ - `prd.markdown` - the full spec (your primary source of truth).
25
+ - `prd.glossary` `[{term, definition}]` - the ubiquitous language to enforce.
26
+ - `grill.requirements` if `grill.done`, else `prd.requirements` `[{id, category, text}]` - the R-IDs to build and trace.
27
+ - `prd.weakestAssumptions` - flag these as known gaps in DECISIONS.md.
28
+ - `brand` - the identity to apply (skip if absent; see ground rules).
29
+ - `bizModel.assumptions` / key `answers` - a one-line business context.
30
+ - `feedback` to-dos and any "Open validation to-dos" in the PRD - known gaps the founder set aside; build around them, do not treat them as blockers.
31
+
32
+ ## The ground rules (honor these exactly)
33
+
34
+ - **Ubiquitous language.** Use the glossary's terms verbatim for every entity, field, route, and UI label. Do not introduce synonyms.
35
+ - **Traceability.** Every requirement has an R-ID. Create `TRACEABILITY.md` mapping each R-ID to the file/component that implements it and how it is verified (a test, a manual check, or "deferred").
36
+ - **Out of scope is absolute.** Do not build past the spec's Out-of-Scope list, even if it looks easy.
37
+ - **Mobile and desktop both work.** Responsive by default; check both viewports.
38
+ - **Brand.** If `state.brand` is present, apply its colors, type, voice, and name throughout. If absent, pick a clean, neutral, accessible default and record the choice in DECISIONS.md.
39
+ - **Ask nothing, decide and record.** The spec is the answer; where it is genuinely silent, choose the simplest option consistent with the PRD and record it in `DECISIONS.md` rather than blocking.
40
+
41
+ ## Build static-first
42
+
43
+ Default to a self-contained static site or a Vite/Astro/static build whose output is one publish directory (`dist/`); set the manifest `hasBackend: false`. Pure HTML/CSS/JS where the interactivity allows; a bundler only when the spec warrants it.
44
+
45
+ **Escalate to a backend ONLY if a requirement genuinely needs one** - apply this three-question test to the R-IDs:
46
+ 1. Does it need a **secret** that cannot ship to the browser (a server-side API key, a payment secret)?
47
+ 2. Does it need **shared or persistent state across users** (a real database, server-stored accounts)?
48
+ 3. Does it need **trusted server-side enforcement** (something the client must not be allowed to fake)?
49
+
50
+ If all three are no, build **static, full stop.** localStorage, static data files, BYOK client-side AI calls, and third-party embeddable widgets do **not** count as a backend - many specs that sound like they need a server can ship a compelling static slice first.
51
+
52
+ **When a backend is genuinely required:** build the static parts anyway, set `hasBackend: true` + `backendNote` in the manifest, and **warn loudly** in chat: the default deploy publishes a static `dist/` to Netlify, so the server part will not deploy that way and needs serverless functions or a separate host. Build the static shell with a clear seam for the backend.
53
+
54
+ ## SEO and discoverability (build all of it, do not skip it)
55
+
56
+ This is the cheap moat. Either run **tl_scaffold** to lay these down for you, or build them by hand into the publish dir, and set each `build.json.seo.*` flag from the file you actually emitted:
57
+ - schema.org JSON-LD as one `@graph` (Organization + Person(founder) cross-referenced, WebSite, and the page type that matches the content); never a type that does not match visible content.
58
+ - `/llms.txt` (title, one-paragraph summary, links to the key pages, a short FAQ).
59
+ - `sitemap.xml`, `robots.txt` (allow search + AI crawlers, point to the sitemap), a canonical link, Open Graph + Twitter meta, and a 1200x630 social image.
60
+ - Semantic, accessible HTML: landmarks, ordered headings, alt text, labelled controls, a visible focus style.
61
+ - A `netlify.toml` (publish dir + SPA redirect) and a `SEO.md` documenting where each item lives and what to fill later (the sameAs links, the social image, the real domain).
62
+
63
+ ## Verify before you call it done
64
+
65
+ Do not declare victory - check:
66
+ 1. **Run the build.** For a bundler: `npm install` then the build command; confirm it exits clean (capture the command into `build.json.buildCommand`). For pure static: confirm the files exist.
67
+ 2. **Confirm the publish dir + entry load.** `dist/index.html` (or your entry) exists and is non-trivial. Where a preview or browser tool is available, load it and confirm it renders; otherwise inspect the built HTML for the expected title/nav/hero and that the mobile viewport meta is set.
68
+ 3. **R-ID coverage.** Walk TRACEABILITY.md: each R-ID is implemented (with a pointer) or explicitly deferred (with a reason). Put the counts in `build.json.requirements`.
69
+ 4. **SEO check.** Confirm the SEO files are actually in the publish dir; set `build.json.seo.*` from reality, not intent.
70
+ 5. **Report** what is built and what is deferred, plainly, in chat.
71
+
72
+ ## Honest about being model-built
73
+
74
+ If you cannot fully build it (the spec is thin, a requirement you genuinely cannot satisfy, time or tool limits), ship **the best static slice that loads** plus DECISIONS.md noting every gap and TRACEABILITY.md marking the unbuilt R-IDs deferred. A partial, honest, deployable artifact beats a complete-looking broken one. **Never fake a green check** - `verified.buildRan: false` is an acceptable, honest value. If even a slice is too much, run **tl_scaffold** to leave a real deployable landing page as the floor.
75
+
76
+ ## Leave a manifest and your decisions
77
+
78
+ Write three files with your own file tools (the manifest is NOT a `tl_state` artifact - it is a plain sidecar):
79
+ - `.thought-layer/build.json` - the deploy contract, co-located with the state file you read. Shape (fill what you can, never fake a field):
80
+
81
+ ```jsonc
82
+ { "app": "thought-layer", "kind": "build", "version": 1, "builtAt": "<ISO>",
83
+ "producer": "agent", "publishDir": "dist", "entry": "index.html",
84
+ "stack": "static|vite|astro|next-static|other", "hasBackend": false, "backendNote": null,
85
+ "buildCommand": null, "installCommand": null, "nodeVersion": "20",
86
+ "provenance": { "stateFile": "<the file you read>", "prdTs": <state.prd.ts>, "grillDone": <bool>, "fromSpeedrun": <bool> },
87
+ "requirements": { "total": 0, "built": 0, "deferred": 0, "deferredIds": [] },
88
+ "seo": { "jsonLd": true, "llmsTxt": true, "sitemap": true, "robots": true, "canonical": true, "openGraph": true, "socialImage": false, "semanticHtml": true, "seoDoc": true, "netlifyToml": true },
89
+ "artifacts": { "traceability": "TRACEABILITY.md", "decisions": "DECISIONS.md", "seo": "SEO.md" },
90
+ "verified": { "buildRan": true, "publishDirExists": true, "entryLoads": true, "notes": "..." } }
91
+ ```
92
+ `publishDir` + `entry` are load-bearing - the deploy step reads them. (The `tl_scaffold` tool writes this same manifest with `producer: "scaffold"`.)
93
+ - `DECISIONS.md` - every choice the spec did not pin down, one line each, with the reason.
94
+ - `TRACEABILITY.md` - the R-ID map. (`SEO.md` comes from the SEO step.)
95
+
96
+ ## Persisting
97
+
98
+ The build output and the three files live on disk; the portable `state.json` stays focused on validation and design. The only optional state touch is a best-effort cursor bump - `tl_state cursor` (or `tl cursor`) with `{ "backboneStage": 15, "phase": "built" }` - pure provenance; if the tool is absent or it fails, the build still succeeded. Tell the user where the artifact is (`<publishDir>`) and that the next step is the deploy (Phase 4, `/tl-deploy`, coming) which reads `build.json`.
@@ -89,6 +89,8 @@ This is where "how will it actually be built" gets answered. Every implementatio
89
89
  14. **The PRD (draft).** Run the **thought-layer-prd** skill: compose a complete first-draft PRD — including a first-cut domain glossary and testable requirements — from the validated idea and the business model above.
90
90
  15. **The Grill.** Run the **thought-layer-grill** skill: grill that draft PRD. Challenge it against the domain one question at a time, sharpen the glossary, surface contradictions, unstated rules, and edge cases, and update the PRD inline until it is build-ready.
91
91
 
92
+ Once the grill has hardened the PRD, the spec is build-ready. Run the **thought-layer-build** skill (`/tl-build`) to turn it into a static-first, deploy-ready artifact (or the `tl_scaffold` tool for an instant deployable landing-page floor). This is the build step, not another validation stage.
93
+
92
94
  ## Supporting passes (run when relevant)
93
95
 
94
96
  Not strictly sequential; pull them in when they help: market research on the segment, a SWOT once the picture is full, and **thought-layer-naming** plus domain checks when the thing needs a name. These inform the stages above; they do not replace them.
@@ -49,6 +49,8 @@ The speedrun gets you all the way to a real spec, fast. The two design steps are
49
49
 
50
50
  What you get is a fast first draft built on a gut-check, not a validated spec. Re-running the grill under `/tl`, with the panels behind it, is what turns it into something to bet on.
51
51
 
52
+ With the PRD hardened you have a build-ready spec - run the **thought-layer-build** skill (`/tl-build`) to build it, or the `tl_scaffold` tool for an instant deployable landing page. (A speedrun spec is a gut-check; re-grill under `/tl` before you bet on it.)
53
+
52
54
  ## Persisting
53
55
 
54
56
  Write to the state file as you go, like the full framework (see the **thought-layer-framework** skill's "Saving and resuming"): answers via op `answer`, the bizModel / naming / brand / prd / grill via op `artifact`, and a cursor via op `cursor` after each stage so the file resumes and upgrades cleanly (use the backbone stage numbers - the spine maps to stages 1, 3, 4, 9, 10, then the PRD is 14 and the grill 15; set `phase` to `speedrun`). The default file is `.thought-layer/state.json`; to keep several ideas apart, pass `--path .thought-layer/<name>.json` (or the tool's `path`) on every op, or set `THOUGHT_LAYER_STATE`, and use `list` to see what is already there. **Do not write graded feedback** - the speedrun does not rank, so there are no panel verdicts to store and the answers persist ungraded.