@hobocode/thought-layer 0.2.1 → 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
+ }
@@ -3,7 +3,7 @@
3
3
  // tl_state tool and the CLI bin. Kept out of progress.ts so the transforms stay
4
4
  // testable without touching disk.
5
5
 
6
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
6
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
7
7
  import { dirname, isAbsolute, join, resolve } from "node:path";
8
8
  import {
9
9
  parseProgress, buildProgress, serializeProgress, emptyState,
@@ -12,15 +12,35 @@ import {
12
12
 
13
13
  export const STATE_DIR = ".thought-layer";
14
14
  export const STATE_FILE = "state.json";
15
+ // Set THOUGHT_LAYER_STATE to a file path to make it the session default, so the
16
+ // agent or user does not have to pass --path on every op when juggling several
17
+ // ideas. Precedence: an explicit target (--path / the tool's `path`) wins, then
18
+ // the env var, then <cwd>/.thought-layer/state.json.
19
+ export const STATE_ENV = "THOUGHT_LAYER_STATE";
20
+
21
+ const withEnv = (target?: string): string | undefined => target ?? (process.env[STATE_ENV] || undefined);
15
22
 
16
23
  // Resolve the canonical state file path. `target` may be a project directory or
17
- // a direct path to a .json file; defaults to <cwd>/.thought-layer/state.json.
24
+ // a direct path to a .json file; with neither, falls back to the env default or
25
+ // <cwd>/.thought-layer/state.json.
18
26
  export function resolveStatePath(target?: string, cwd: string = process.cwd()): string {
19
- if (!target) return join(cwd, STATE_DIR, STATE_FILE);
20
- const abs = isAbsolute(target) ? target : resolve(cwd, target);
27
+ const t = withEnv(target);
28
+ if (!t) return join(cwd, STATE_DIR, STATE_FILE);
29
+ const abs = isAbsolute(t) ? t : resolve(cwd, t);
21
30
  return abs.endsWith(".json") ? abs : join(abs, STATE_DIR, STATE_FILE);
22
31
  }
23
32
 
33
+ // List the state files under <dir>/.thought-layer/ so several ideas can live
34
+ // side by side and be discovered. `dir` is a project directory (defaults to cwd).
35
+ export function listStateFiles(dir: string = process.cwd()): Array<{ name: string; path: string }> {
36
+ const d = join(dir, STATE_DIR);
37
+ if (!existsSync(d)) return [];
38
+ return readdirSync(d)
39
+ .filter((f) => f.endsWith(".json"))
40
+ .sort()
41
+ .map((name) => ({ name, path: join(d, name) }));
42
+ }
43
+
24
44
  export interface LoadResult {
25
45
  path: string;
26
46
  exists: boolean;
package/core/state-ops.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  // core/state-file.ts. Pure of any host concern (no Pi types, no argv).
6
6
 
7
7
  import { gradeFromConfidence } from "./scoring.ts";
8
- import { loadStateFile, saveStateFile } from "./state-file.ts";
8
+ import { loadStateFile, saveStateFile, listStateFiles, STATE_DIR, STATE_ENV } from "./state-file.ts";
9
9
  import {
10
10
  setAnswer, setArtifact, setCursor, parkNote, buildFeedbackEntry,
11
11
  normalizeArtifactValue, summarizeState,
@@ -40,17 +40,39 @@ export function applyStateOp(p: StateOp, ctx: { ts: number; exportedAt: string }
40
40
  const { ts, exportedAt } = ctx;
41
41
  const fail = (message: string): StateOpResult => ({ ok: false, message, details: {} });
42
42
  try {
43
+ if (p.op === "list") {
44
+ const files = listStateFiles(p.path);
45
+ if (!files.length) {
46
+ return { ok: true, message: `No state files under ./${STATE_DIR}/. A fresh run creates ${STATE_DIR}/state.json; pass --path <file>.json (or set ${STATE_ENV}) to use another.`, details: { files: [] } };
47
+ }
48
+ const rows = files.map((f) => {
49
+ try {
50
+ const sum = summarizeState(loadStateFile(f.path).state);
51
+ return { name: f.name, path: f.path, answered: sum.answered, artifacts: sum.artifacts };
52
+ } catch {
53
+ return { name: f.name, path: f.path, answered: 0, artifacts: [] as string[], unreadable: true };
54
+ }
55
+ });
56
+ const lines = rows.map((r) => ` ${r.name} - ${r.answered} answered${r.artifacts.length ? `, artifacts: ${r.artifacts.join(", ")}` : ""}${("unreadable" in r) ? " (unreadable)" : ""}`).join("\n");
57
+ return { ok: true, message: `${files.length} state file(s) under ./${STATE_DIR}/:\n${lines}\nPick one with --path .thought-layer/<name>.json, or set ${STATE_ENV} for the session.`, details: { files: rows } };
58
+ }
59
+
43
60
  const loaded = loadStateFile(p.path);
44
61
  const save = (next: typeof loaded.state) => saveStateFile(next, { target: p.path, ts, exportedAt }).path;
45
62
 
46
63
  if (p.op === "read" || p.op === "export") {
47
64
  const sum = summarizeState(loaded.state);
48
- const message = loaded.exists
49
- ? `Loaded ${loaded.path}: ${sum.answered}/${sum.totalAnswerable} answered ` +
65
+ let message: string;
66
+ if (loaded.exists) {
67
+ message = `Loaded ${loaded.path}: ${sum.answered}/${sum.totalAnswerable} answered ` +
50
68
  `(${sum.byStatus.green} green, ${sum.byStatus.yellow} yellow, ${sum.byStatus.red} red), ` +
51
69
  `artifacts: ${sum.artifacts.join(", ") || "none"}. ` +
52
- `Resume at ${sum.cursor ? `stage ${sum.cursor.backboneStage ?? "?"} (${sum.cursor.phase ?? "?"})` : "the beginning"}.`
53
- : `No state file yet at ${loaded.path}. Start a fresh run; it will be created on first write.`;
70
+ `Resume at ${sum.cursor ? `stage ${sum.cursor.backboneStage ?? "?"} (${sum.cursor.phase ?? "?"})` : "the beginning"}.`;
71
+ } else {
72
+ const others = listStateFiles().filter((f) => f.path !== loaded.path);
73
+ const hint = others.length ? ` Other state files here: ${others.map((f) => f.name).join(", ")} (pick one with --path, or 'tl list').` : "";
74
+ message = `No state file yet at ${loaded.path}.${hint} Start a fresh run; it will be created on first write.`;
75
+ }
54
76
  return { ok: true, message, details: { path: loaded.path, exists: loaded.exists, summary: sum, state: loaded.state } };
55
77
  }
56
78
 
package/dist/tl.js CHANGED
@@ -26,7 +26,7 @@ function aggregateConfidence(values) {
26
26
  }
27
27
 
28
28
  // core/state-file.ts
29
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
29
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "fs";
30
30
  import { dirname, isAbsolute, join, resolve } from "path";
31
31
 
32
32
  // core/stage-map.ts
@@ -282,11 +282,19 @@ function parkNote(state, key, note, ts) {
282
282
  // core/state-file.ts
283
283
  var STATE_DIR = ".thought-layer";
284
284
  var STATE_FILE = "state.json";
285
+ var STATE_ENV = "THOUGHT_LAYER_STATE";
286
+ var withEnv = (target) => target ?? (process.env[STATE_ENV] || void 0);
285
287
  function resolveStatePath(target, cwd = process.cwd()) {
286
- if (!target) return join(cwd, STATE_DIR, STATE_FILE);
287
- const abs = isAbsolute(target) ? target : resolve(cwd, target);
288
+ const t = withEnv(target);
289
+ if (!t) return join(cwd, STATE_DIR, STATE_FILE);
290
+ const abs = isAbsolute(t) ? t : resolve(cwd, t);
288
291
  return abs.endsWith(".json") ? abs : join(abs, STATE_DIR, STATE_FILE);
289
292
  }
293
+ function listStateFiles(dir = process.cwd()) {
294
+ const d = join(dir, STATE_DIR);
295
+ if (!existsSync(d)) return [];
296
+ return readdirSync(d).filter((f) => f.endsWith(".json")).sort().map((name) => ({ name, path: join(d, name) }));
297
+ }
290
298
  function loadStateFile(target, cwd) {
291
299
  const path = resolveStatePath(target, cwd);
292
300
  if (!existsSync(path)) {
@@ -313,11 +321,36 @@ function applyStateOp(p, ctx) {
313
321
  const { ts, exportedAt } = ctx;
314
322
  const fail = (message) => ({ ok: false, message, details: {} });
315
323
  try {
324
+ if (p.op === "list") {
325
+ const files = listStateFiles(p.path);
326
+ if (!files.length) {
327
+ return { ok: true, message: `No state files under ./${STATE_DIR}/. A fresh run creates ${STATE_DIR}/state.json; pass --path <file>.json (or set ${STATE_ENV}) to use another.`, details: { files: [] } };
328
+ }
329
+ const rows = files.map((f) => {
330
+ try {
331
+ const sum = summarizeState(loadStateFile(f.path).state);
332
+ return { name: f.name, path: f.path, answered: sum.answered, artifacts: sum.artifacts };
333
+ } catch {
334
+ return { name: f.name, path: f.path, answered: 0, artifacts: [], unreadable: true };
335
+ }
336
+ });
337
+ const lines = rows.map((r) => ` ${r.name} - ${r.answered} answered${r.artifacts.length ? `, artifacts: ${r.artifacts.join(", ")}` : ""}${"unreadable" in r ? " (unreadable)" : ""}`).join("\n");
338
+ return { ok: true, message: `${files.length} state file(s) under ./${STATE_DIR}/:
339
+ ${lines}
340
+ Pick one with --path .thought-layer/<name>.json, or set ${STATE_ENV} for the session.`, details: { files: rows } };
341
+ }
316
342
  const loaded = loadStateFile(p.path);
317
343
  const save = (next) => saveStateFile(next, { target: p.path, ts, exportedAt }).path;
318
344
  if (p.op === "read" || p.op === "export") {
319
345
  const sum = summarizeState(loaded.state);
320
- const message = loaded.exists ? `Loaded ${loaded.path}: ${sum.answered}/${sum.totalAnswerable} answered (${sum.byStatus.green} green, ${sum.byStatus.yellow} yellow, ${sum.byStatus.red} red), artifacts: ${sum.artifacts.join(", ") || "none"}. Resume at ${sum.cursor ? `stage ${sum.cursor.backboneStage ?? "?"} (${sum.cursor.phase ?? "?"})` : "the beginning"}.` : `No state file yet at ${loaded.path}. Start a fresh run; it will be created on first write.`;
346
+ let message;
347
+ if (loaded.exists) {
348
+ message = `Loaded ${loaded.path}: ${sum.answered}/${sum.totalAnswerable} answered (${sum.byStatus.green} green, ${sum.byStatus.yellow} yellow, ${sum.byStatus.red} red), artifacts: ${sum.artifacts.join(", ") || "none"}. Resume at ${sum.cursor ? `stage ${sum.cursor.backboneStage ?? "?"} (${sum.cursor.phase ?? "?"})` : "the beginning"}.`;
349
+ } else {
350
+ const others = listStateFiles().filter((f) => f.path !== loaded.path);
351
+ const hint = others.length ? ` Other state files here: ${others.map((f) => f.name).join(", ")} (pick one with --path, or 'tl list').` : "";
352
+ message = `No state file yet at ${loaded.path}.${hint} Start a fresh run; it will be created on first write.`;
353
+ }
321
354
  return { ok: true, message, details: { path: loaded.path, exists: loaded.exists, summary: sum, state: loaded.state } };
322
355
  }
323
356
  if (p.op === "answer") {
@@ -364,10 +397,284 @@ function applyStateOp(p, ctx) {
364
397
  }
365
398
  }
366
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
+
367
672
  // bin/tl.ts
368
- var HELP = `tl - read/write the portable Thought Layer state file (.thought-layer/state.json)
673
+ var HELP = `tl - read/write a portable Thought Layer state file (default: .thought-layer/state.json)
369
674
 
370
675
  tl read [path] [--json] where the run stands
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
371
678
  tl export [path] handoff check
372
679
  tl answer <qId> <value> [path] record an answer
373
680
  tl feedback --data '<json>' record a panel verdict ({qId,mode,personas,endState,round})
@@ -376,6 +683,9 @@ var HELP = `tl - read/write the portable Thought Layer state file (.thought-laye
376
683
  tl park <key> <note> [path] stash a panel note
377
684
  tl exec --data '<json>' run a full {op,...} payload
378
685
 
686
+ Selecting a file: pass --path <file>.json (or a positional path) to any op, keep several
687
+ ideas as .thought-layer/<name>.json, or set THOUGHT_LAYER_STATE once as the session default.
688
+
379
689
  --path <p> project dir or .json path --data <j> JSON payload ("-" = stdin)
380
690
  --json print details JSON -h, --help`;
381
691
  function parseArgs(argv) {
@@ -414,6 +724,8 @@ function buildOp(args, flags) {
414
724
  case "read":
415
725
  case "export":
416
726
  return { op, path: path ?? args[1] };
727
+ case "list":
728
+ return { op, path: path ?? args[1] };
417
729
  case "answer":
418
730
  return { op, qId: args[1], value: args[2], path: path ?? args[3] };
419
731
  case "park":
@@ -437,6 +749,20 @@ function main() {
437
749
  console.log(HELP);
438
750
  process.exit(0);
439
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
+ }
440
766
  let payload;
441
767
  try {
442
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
 
@@ -148,16 +148,16 @@ export default function (pi: ExtensionAPI) {
148
148
  name: "tl_state",
149
149
  label: "Thought Layer: state",
150
150
  description:
151
- "Read, update, and write the portable Thought Layer progress file (.thought-layer/state.json) shared with the web app so work passes losslessly between a founder in the browser and an agent. " +
152
- "ops: 'read' (resume: where the run stands), 'answer' (record a question answer), 'feedback' (record a panel verdict - pass it the per-persona prose + confidences and it builds the exact entry), " +
151
+ "Read, update, and write a portable Thought Layer progress file (default .thought-layer/state.json) shared with the web app so work passes losslessly between a founder in the browser and an agent. " +
152
+ "ops: 'read' (resume: where the run stands), 'list' (list the state files under .thought-layer/ when juggling several ideas), 'answer' (record a question answer), 'feedback' (record a panel verdict - pass it the per-persona prose + confidences and it builds the exact entry), " +
153
153
  "'artifact' (store prd/grill/bizModel/naming/brand/etc., requirements auto-normalized), 'cursor' (save resume position), 'park' (stash a panel note with no web-app question), 'export' (report the current file for handoff). " +
154
- "Always use this instead of writing the JSON by hand.",
154
+ "To juggle several ideas, give each its own file via `path` (e.g. .thought-layer/acme.json) and use the same path for every op in the session. Always use this instead of writing the JSON by hand.",
155
155
  parameters: Type.Object({
156
156
  op: Type.Union([
157
- Type.Literal("read"), Type.Literal("answer"), Type.Literal("feedback"),
157
+ Type.Literal("read"), Type.Literal("list"), Type.Literal("answer"), Type.Literal("feedback"),
158
158
  Type.Literal("artifact"), Type.Literal("cursor"), Type.Literal("park"), Type.Literal("export"),
159
159
  ], { description: "The operation to perform." }),
160
- path: Type.Optional(Type.String({ description: "Project dir or .json path. Defaults to ./.thought-layer/state.json in the cwd." })),
160
+ path: Type.Optional(Type.String({ description: "Project dir or .json path; selects WHICH state file to use. Defaults to ./.thought-layer/state.json. Use a named file (e.g. .thought-layer/acme.json) to keep ideas separate; for 'list', a project dir to scan." })),
161
161
  qId: Type.Optional(Type.String({ description: "Question id (for 'answer'/'feedback'). Must be a real Thought Layer question id." })),
162
162
  value: Type.Optional(Type.Unknown({ description: "For 'answer': the answer string. For 'artifact': the artifact object." })),
163
163
  artifact: Type.Optional(Type.String({ description: "For 'artifact': one of bizModel, grill, assets, research, swot, prd, naming, brand." })),
@@ -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.1",
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,9 @@
1
+ Run the Thought Layer speedrun on the idea below using the **thought-layer-speedrun** skill.
2
+
3
+ The speedrun is the fast path to a build-ready spec: a condensed five-stage validation spine (what it is, will anyone pay, the market, the price, do the numbers work) with fast unranked feedback - one honest read per stage, no panel, no `tl_score`, no grade, no 0.85 gate, and the user moves on when they are ready. After the five stages, ask whether to run the **thought-layer-brand** skill, then run **thought-layer-prd** and **thought-layer-grill** to reach a hardened, build-ready PRD. Do not skip the PRD and the grill - reaching the spec fast is the whole point, not stopping short of it. The same unranked, user-driven mode applies through the brand pass, the PRD, and the grill: ignore those skills' panel, scoring, gate, and stop-condition machinery (keep only the grill's metric-honesty rule), and let the user's "move on" end each step.
4
+
5
+ Take it one stage at a time, one stage per turn, and wait for the user between stages. Persist answers and artifacts to the shared state file as you go (the question ids match the web app). The speedrun trades the panel's defensible conviction for speed, so close by pointing the user at the full `/tl` for anything they will actually build.
6
+
7
+ If an idea is given below, treat it as the answer to stage 1 (the What) and give it a fast read before moving on. If nothing is below, ask the user for their idea in one sentence - do not wait to be handed a brief.
8
+
9
+ The idea (if any):
@@ -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`.
@@ -24,9 +24,11 @@ Reaching the Grill or the PRD before all of Part 1 (validate the idea) and Part
24
24
 
25
25
  No one finishes this in one sitting. The work lives in a portable file, `.thought-layer/state.json`, in the project directory. It is your memory across turns and sessions, and it is the SAME interop file the web app reads, so a founder can answer some stages here and hand the file to a co-founder who continues in the web app (weareallproductmanagersnow.com, "Load progress from file"), back and forth, losslessly. Never hand-write this JSON: use the tool below, which builds the exact shapes the web app expects.
26
26
 
27
- **The tool.** If the `tl_state` tool is available (Pi), use it. Otherwise run the CLI from any shell: `npx -y @hobocode/thought-layer tl <op> ...` (or just `tl ...` if the package is installed). Ops: `read`, `answer`, `feedback`, `artifact`, `cursor`, `park`, `export`.
27
+ **The tool.** If the `tl_state` tool is available (Pi), use it. Otherwise run the CLI from any shell: `npx -y @hobocode/thought-layer tl <op> ...` (or just `tl ...` if the package is installed). Ops: `read`, `list`, `answer`, `feedback`, `artifact`, `cursor`, `park`, `export`.
28
28
 
29
- **On start, ALWAYS read first.** `tl_state read` (or `tl read`). If a file exists, summarize where the run stands and **resume from the saved cursor** - do not restart at stage 1. If not, start fresh; the file is created on first write.
29
+ **Choosing the file.** The default is `.thought-layer/state.json`. To keep several ideas side by side, give each its own file and use the SAME path for every op in the session: pass `--path .thought-layer/<name>.json` (or the tool's `path`), or set `THOUGHT_LAYER_STATE` once as the session default. `list` shows the files already there.
30
+
31
+ **On start, ALWAYS read first.** `tl_state read` (or `tl read`). If a file exists, summarize where the run stands and **resume from the saved cursor** - do not restart at stage 1. If not, start fresh; the file is created on first write. If `list` shows more than one state file, ask which idea to resume (or to start a new one) before reading, and stick to that path for the rest of the session.
30
32
 
31
33
  **After each stage:**
32
34
  1. Record the answer against its question id: `tl_state` op `answer` (or `tl answer <qId> "<value>"`).
@@ -87,6 +89,8 @@ This is where "how will it actually be built" gets answered. Every implementatio
87
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.
88
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.
89
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
+
90
94
  ## Supporting passes (run when relevant)
91
95
 
92
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.
@@ -0,0 +1,62 @@
1
+ ---
2
+ name: thought-layer-speedrun
3
+ description: "Run the Thought Layer at speed: a condensed five-stage validation spine (what it is, will anyone pay, market, price, do the numbers work) with fast unranked feedback - no panel, no score, no 0.85 gate, you move on when ready - then an optional brand pass and the PRD plus grill, so you still reach a build-ready spec. For the impatient and for demos. It trades the panel's defensible conviction for speed; run the full thought-layer-framework on anything you will actually build."
4
+ ---
5
+
6
+ # The Thought Layer speedrun
7
+
8
+ The fast path to a build-ready spec. You still reach the same SHAPE of output the full framework produces - a PRD hardened by the grill - but the validation in front of it is compressed to five fast, unranked stages, and there is far less conviction behind it. No panel, no confidence score, no 0.85 gate: one honest read per stage, and you move on whenever you want.
9
+
10
+ For the impatient, and a good way to see the whole arc - validation to spec - in one sitting (a demo). It trades the panel's defensible conviction for speed: the spec is real and buildable, but the validation behind it is a gut-check, not an adversarial panel. For anything you will actually build, run the full **thought-layer-framework** (`/tl`) - the rigor is the part that was ever scarce. Speedrun is the taste and the on-ramp.
11
+
12
+ ## How the fast feedback works
13
+
14
+ For each of the five stages, give ONE honest read - not three personas, no `tl_score`, no grade, no gate:
15
+
16
+ - The single strongest thing about the answer, and the single weakest.
17
+ - At most one sharp question or fix that would most improve it.
18
+ - Then ask: "sharpen it, or move on?" The user decides, every time. Keep offering feedback as they revise; never force a bar, never auto-advance.
19
+
20
+ Stay honest and stay fast: a few sentences, not an audit. **Fast does not mean kind** - a fatal flaw gets named plainly even if everything else gets one sentence. Park concerns that belong to the design phase in a line ("we'll catch that in the grill") rather than raising them now.
21
+
22
+ **This no-panel, no-score, no-gate, user-driven rule holds for the ENTIRE speedrun - the brand pass, the PRD, and the grill included.** Every sub-skill you invoke below runs in this unranked, user-driven mode: ignore its panel, `tl_score`, 0.85-gate, disqualifier-loop, and "categories covered" stop machinery, and let the user's "move on" end each step. The one exception is the grill's **metric-honesty rule**, kept below - it is cheap and load-bearing.
23
+
24
+ ## The five-stage spine
25
+
26
+ One stage per turn, in order, waiting for the user between stages. Record each answer to the shared state file as you go (op `answer`; see "Persisting"). The question ids match the web app exactly, so a speedrun loads straight into it. The spine deliberately skips the framework's domain-knowledge, time, costs, scale, acquisition, relationships, and support stages; those stay blank by design, so a loaded file that reads as partially complete is expected, not an error. If a skipped stage holds something that genuinely blocks the spec, raise it in one line instead of re-opening the stage.
27
+
28
+ 1. **The What** (`what-statement`) - "One sentence: a thing that does what, for whom." Clear and specific, not the pitch.
29
+ 2. **Will anyone pay?** (`paid-today`, `evidence`) - "Has anyone paid you to solve this, or what is the strongest signal they will?" Liking is not buying.
30
+ 3. **The market** (`target-market`, `incumbent-gap`) - "Who specifically is this for, and why won't an incumbent just copy it?" Start with the smallest market you can dominate.
31
+ 4. **The price** (`pricing-model`) - "What is the number, and can you defend it in one sentence without apologizing?"
32
+ 5. **Do the numbers move?** (`bm-who-buys`, `bm-who-supplies`) - "Name the price, a plausible customer count, and the main monthly costs as real numbers: does revenue clear cost, and roughly when?" The honest read is whether those are real estimates or hopeful blanks. Use the `tl_project` tool for a quick projection and store it as the `bizModel` artifact if useful. (`bm-parties` is intentionally left for the full mode, so the `/tl` upgrade stays lossless.)
33
+
34
+ After stage 5, before the design phase, branch on brand.
35
+
36
+ ## Before the PRD: ask about brand
37
+
38
+ Ask the user once: **"Want to name it and sketch a quick brand before we spec it?"**
39
+
40
+ - If yes, run the **thought-layer-brand** skill (which wraps **thought-layer-naming** + the `tl_domains` tool) - for its stages and questions only: suppress its panel, `tl_score`, 0.85 gate, and disqualifier loop; one fast unranked read per brand step, and the user moves on when they like a direction. A speedrun brand is a directional sketch off an unvalidated idea - re-test it under `/tl` before you print it on anything. Its output feeds the PRD's identity and UX notes and persists as the `naming` and `brand` artifacts.
41
+ - If no, go straight to the PRD.
42
+
43
+ ## The end - do NOT skip this, it is the whole point
44
+
45
+ The speedrun gets you all the way to a real spec, fast. The two design steps are not optional.
46
+
47
+ 6. **The PRD.** Run the **thought-layer-prd** skill to compose the draft from the five answers (and the brand, if you ran it). Store it as the `prd` artifact.
48
+ 7. **The grill.** Run the **thought-layer-grill** skill to harden it, speedrun-style. **In speedrun mode the user's "good enough, ship it" ends the grill, not category coverage - this overrides the grill skill's own stop rule.** Hit only the highest-risk gaps and contradictions; do not pursue completeness. Keep the grill's **metric-honesty rule** (the cheap backstop against vanity metrics); relax only the exhaustive-coverage stop and the gate. Update the PRD inline and re-store the `prd` artifact when done.
49
+
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
+
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
+
54
+ ## Persisting
55
+
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.
57
+
58
+ This keeps a speedrun loadable in the web app and upgradable. If you resume a file that already has panel feedback or a `/tl` cursor, do not discard it: tell the user which stages are already graded and only speedrun the still-open ones. If the user stops mid-spine, the answers so far are already saved - tell them the file location and that they can resume here or upgrade to `/tl` anytime.
59
+
60
+ ## Demo mode
61
+
62
+ If the user just wants to see how this works, offer to run the whole thing on a sample idea (suggest one, for example a scheduling tool for mobile dog groomers) so they can watch the arc - five fast reads, an optional brand, a PRD, a quick grill - in a couple of minutes. It is still one stage per turn, pausing for the user: demo mode is fast, not unattended. The sample writes a real `state.json`, so clear it (or run it in a throwaway directory) before the user starts on their own idea.