@hobocode/thought-layer 0.2.0 → 0.2.2

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.
@@ -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") {
@@ -365,9 +398,10 @@ function applyStateOp(p, ctx) {
365
398
  }
366
399
 
367
400
  // bin/tl.ts
368
- var HELP = `tl - read/write the portable Thought Layer state file (.thought-layer/state.json)
401
+ var HELP = `tl - read/write a portable Thought Layer state file (default: .thought-layer/state.json)
369
402
 
370
403
  tl read [path] [--json] where the run stands
404
+ tl list [dir] list the state files under .thought-layer/ (juggle several ideas)
371
405
  tl export [path] handoff check
372
406
  tl answer <qId> <value> [path] record an answer
373
407
  tl feedback --data '<json>' record a panel verdict ({qId,mode,personas,endState,round})
@@ -376,6 +410,9 @@ var HELP = `tl - read/write the portable Thought Layer state file (.thought-laye
376
410
  tl park <key> <note> [path] stash a panel note
377
411
  tl exec --data '<json>' run a full {op,...} payload
378
412
 
413
+ Selecting a file: pass --path <file>.json (or a positional path) to any op, keep several
414
+ ideas as .thought-layer/<name>.json, or set THOUGHT_LAYER_STATE once as the session default.
415
+
379
416
  --path <p> project dir or .json path --data <j> JSON payload ("-" = stdin)
380
417
  --json print details JSON -h, --help`;
381
418
  function parseArgs(argv) {
@@ -414,6 +451,8 @@ function buildOp(args, flags) {
414
451
  case "read":
415
452
  case "export":
416
453
  return { op, path: path ?? args[1] };
454
+ case "list":
455
+ return { op, path: path ?? args[1] };
417
456
  case "answer":
418
457
  return { op, qId: args[1], value: args[2], path: path ?? args[3] };
419
458
  case "park":
@@ -432,6 +471,7 @@ function buildOp(args, flags) {
432
471
  }
433
472
  function main() {
434
473
  const { args, flags } = parseArgs(process.argv.slice(2));
474
+ if (args[0] === "tl" || args[0] === "thought-layer") args.shift();
435
475
  if (flags["help"] || args.length === 0) {
436
476
  console.log(HELP);
437
477
  process.exit(0);
@@ -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." })),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hobocode/thought-layer",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
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>",
@@ -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):
@@ -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>"`).
@@ -0,0 +1,60 @@
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
+ ## Persisting
53
+
54
+ 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.
55
+
56
+ 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.
57
+
58
+ ## Demo mode
59
+
60
+ 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.