@hobocode/thought-layer 0.4.1 → 0.5.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
@@ -17,6 +17,10 @@ This is open source and BYOK by design. The point is to help people build real t
17
17
  - **thought-layer-prd.** Draft the complete PRD — with a first-cut domain glossary and testable requirements — from the validated idea and business model. The plan the grill then hardens.
18
18
  - **thought-layer-grill.** The last design step: grills the draft PRD against the domain one question at a time, sharpening the glossary and hardening the requirements inline until it is build-ready. Runs after the PRD, not instead of the framework.
19
19
  - **thought-layer-naming.** Name the thing, with rationale and domain-ready slugs.
20
+ - **thought-layer-build.** Build the hardened PRD into a static-first, deploy-ready artifact, verified to run, and leave a manifest the deploy step reads. When a requirement genuinely needs a server, it also emits a real backend (serverless functions, a `schema.sql`, a names-only `.env.example`, and a `BACKEND.md` guide), with Neon Postgres as the documented default.
21
+ - **thought-layer-deploy.** Take the build live to a URL you own, with no lock-in: a Netlify token deploys into your own account, or the Netlify CLI handles a logged-in or anonymous deploy.
22
+ - **thought-layer-speedrun.** A fast, unranked path to a build-ready spec when you do not need the full panel and score.
23
+ - **Optional deep-dives**, pulled in when you want to go further than the backbone: `thought-layer-strategy`, `thought-layer-brand`, `thought-layer-market-research`, and `thought-layer-business-model`.
20
24
 
21
25
  **A Pi package** that adds, on top of the skills:
22
26
 
@@ -30,11 +34,11 @@ This is open source and BYOK by design. The point is to help people build real t
30
34
 
31
35
  ```bash
32
36
  pi install npm:@hobocode/thought-layer
33
- # or, before it is published:
37
+ # or track the latest from GitHub:
34
38
  pi install git:github.com/hobocode-ofc/thought-layer-kit
35
39
  ```
36
40
 
37
- Installing the package lights up the skills, the `/tl` commands, and the `tl_score` / `tl_domains` / `tl_project` tools. You can also invoke a skill directly with `/skill:thought-layer-panel`.
41
+ Installing the package lights up the skills, the `/tl` commands, and the deterministic tools (`tl_score`, `tl_domains`, `tl_project`, `tl_state`, `tl_scaffold`, `deploy`). You can also invoke a skill directly with `/skill:thought-layer-panel`.
38
42
 
39
43
  ### Claude Code (or any agent that reads the Agent Skills format)
40
44
 
@@ -65,6 +69,8 @@ The hosted version of the rigor lives at [weareallproductmanagersnow.com](https:
65
69
  - **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
70
  - **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.
67
71
  - **Phase 4 (done):** a `deploy` step (`/tl-deploy`, the `deploy` tool, or `tl deploy`) that takes the build live to a URL you own, closing the loop. With a Netlify token it deploys into your own account via the file-digest API (owned immediately, no claim step); with no token it delegates to your Netlify CLI - logged in it creates a site in your account, logged out it deploys anonymously with a one-hour claim link. BYOK, no central account, no lock-in. `--dry-run` shows the plan first.
72
+ - **Backend-capable build (done):** when the three-question backend test shows a product genuinely needs a server, `/tl-build` emits a real backend alongside the static front end (serverless functions per backend requirement, a `schema.sql`, a names-only `.env.example`, an updated `netlify.toml`, and a `BACKEND.md` guide), with Neon Postgres as the documented default and overridable to any Postgres. Static stays the default, gated by the same backend test.
73
+ - **Next:** automated backend deploy. `tl deploy` ships the static front end today and points at `BACKEND.md` for the one-time backend steps; teaching the deploy step to provision the database and ship the functions into your own account is the follow-up.
68
74
 
69
75
  ## Notes for contributors
70
76
 
package/SECURITY.md ADDED
@@ -0,0 +1,50 @@
1
+ # Security
2
+
3
+ The Thought Layer Kit is bring-your-own-key by design. It has no server, no
4
+ telemetry, and no central account. The threat model and the guarantees below
5
+ follow from that.
6
+
7
+ ## What the kit handles
8
+
9
+ - **Secrets are read from the environment only.** The Netlify token
10
+ (`NETLIFY_AUTH_TOKEN` / `NETLIFY_TOKEN`) and the domain-check key
11
+ (`THOUGHT_LAYER_DOMAIN_KEY` / `RAPIDAPI_KEY`) are read from `process.env`. They
12
+ are never accepted as tool or CLI parameters, never logged or printed, and
13
+ never written to disk. The `deploy.json` record stores the resulting URLs and
14
+ ids, never the token. When a build emits a backend, the generated `.env.example`
15
+ is a names-only contract (every line is a bare `NAME=`): real values live only
16
+ in the host environment, never in a committed file.
17
+ - **Deploys go to your own account.** With a token, the deploy uses Netlify's
18
+ file-digest API to publish into your account. With no token, it delegates to
19
+ your installed Netlify CLI (a site in your account when logged in, or an
20
+ anonymous, claimable site when logged out). Nothing is hosted on infrastructure
21
+ we control, and there is no claim handshake we mediate.
22
+ - **No shell injection.** External commands (the Netlify CLI) are invoked with an
23
+ argument array and no shell, so values such as a site name or publish directory
24
+ cannot break out into a shell. Site names are sanitized to `[a-z0-9-]`.
25
+ - **File writes are confined.** The scaffold and state-file writers resolve paths
26
+ against the working directory; the state file and build artifacts live under
27
+ `.thought-layer/` and the chosen publish directory.
28
+ - **No network calls you did not ask for.** The only outbound requests are to the
29
+ Netlify API (deploy) and, if you set a domain key, the RapidAPI domains
30
+ endpoint. There is no analytics or phone-home.
31
+
32
+ ## What stays your responsibility
33
+
34
+ - The kit runs inside your own agent (Pi, Claude Code, or another) on your own
35
+ model and keys. The quality and safety of code an agent builds from a spec is a
36
+ function of that agent and model, not the kit.
37
+ - Keep your provider keys and Netlify token in your environment or your agent's
38
+ secret store, not in committed files. `.thought-layer/` and `.env` are
39
+ gitignored in this repo for that reason.
40
+
41
+ ## Reporting a vulnerability
42
+
43
+ Email security reports to **jerm@hobocode.net**. Please include steps to
44
+ reproduce and the affected version. We aim to acknowledge within a few days.
45
+ Public disclosure is welcome once a fix is released.
46
+
47
+ ## Supported versions
48
+
49
+ The latest published `@hobocode/thought-layer` release on npm is the supported
50
+ version. Fixes ship forward; please update before reporting.
@@ -0,0 +1,272 @@
1
+ // The typed contract + deterministic boilerplate for the backend-capable build.
2
+ //
3
+ // Static stays the default. When the build skill's three-question backend test
4
+ // says a product genuinely needs a server (a browser-unsafe secret, shared or
5
+ // persistent state, or trusted server-side enforcement), the agent emits a real
6
+ // serverless backend alongside the static front end. This module is the
7
+ // deterministic floor for that path, mirroring scaffold.ts: the agent supplies
8
+ // judgment (which functions, which tables), and these pure helpers render the
9
+ // fixed, dash-free boilerplate (the env contract and the deploy guide) and
10
+ // coerce a hand-written backend block into a clean shape.
11
+ //
12
+ // Pure TypeScript, no node imports. The chosen runtime DB driver
13
+ // (`@neondatabase/serverless`) is a dependency of the GENERATED product, never
14
+ // of the kit. Nothing here holds or provisions credentials: secrets live only
15
+ // in the host environment, and the emitted .env.example carries names, never
16
+ // values.
17
+ //
18
+ // Copy rule (enforced by tests): no em-dashes, no en-dashes, and no spaced
19
+ // hyphen dashes (" - ", " -- ") in any generated prose. Use commas, colons,
20
+ // periods, and parentheses instead.
21
+
22
+ export type BackendKind = "serverless" | "server" | null;
23
+
24
+ // One environment variable the backend reads. Names only: a value never lives
25
+ // in this spec, the manifest, or the emitted .env.example. Real values stay in
26
+ // the host environment.
27
+ export interface EnvVarSpec {
28
+ name: string;
29
+ required: boolean;
30
+ description: string;
31
+ }
32
+
33
+ // Where the backend keeps shared or persistent state. The kit's documented
34
+ // default is Neon Postgres, reached through the `DATABASE_URL` connection
35
+ // string. Overridable to any Postgres by pointing `DATABASE_URL` elsewhere.
36
+ export interface DatabaseSpec {
37
+ provider: string; // "neon" by default
38
+ schemaFile: string; // "schema.sql"
39
+ envVar: string; // "DATABASE_URL"
40
+ }
41
+
42
+ // The structured backend payload carried on the build manifest. The build skill
43
+ // writes it when hasBackend is true; the deploy-automation follow-up consumes
44
+ // it. Today the deploy step only reads it for messaging.
45
+ export interface BackendMeta {
46
+ backendKind: BackendKind;
47
+ functionsDir: string; // "netlify/functions"
48
+ runtime: string; // "nodejs20.x"
49
+ nodeVersion: string; // "20"
50
+ envVars: EnvVarSpec[];
51
+ database: DatabaseSpec | null;
52
+ guide: string; // "BACKEND.md"
53
+ }
54
+
55
+ // The single documented default database. Neon is also exactly what managed
56
+ // Netlify DB provisions, so it is both the Netlify native choice and portable.
57
+ export const NEON_DEFAULT_DB: DatabaseSpec = {
58
+ provider: "neon",
59
+ schemaFile: "schema.sql",
60
+ envVar: "DATABASE_URL",
61
+ };
62
+
63
+ // ---- helpers -----------------------------------------------------------------
64
+
65
+ // Coerce a name to a safe environment-variable identifier. Real env var names
66
+ // are UPPER_SNAKE; this hardens the names-only invariant so a stray space or
67
+ // lowercase letter can never produce a line that fails the `^[A-Z0-9_]+=$`
68
+ // security check.
69
+ function envName(raw: string): string {
70
+ const cleaned = String(raw || "")
71
+ .trim()
72
+ .toUpperCase()
73
+ .replace(/[^A-Z0-9_]+/g, "_")
74
+ .replace(/^_+|_+$/g, "");
75
+ return cleaned || "VAR";
76
+ }
77
+
78
+ // Dedupe by safe name and sort, so the same set of vars always renders byte for
79
+ // byte the same regardless of input order.
80
+ function dedupeSortEnv(envVars: EnvVarSpec[]): EnvVarSpec[] {
81
+ const seen = new Set<string>();
82
+ const out: EnvVarSpec[] = [];
83
+ for (const v of envVars || []) {
84
+ const name = envName(v?.name ?? "");
85
+ if (seen.has(name)) continue;
86
+ seen.add(name);
87
+ out.push({ name, required: v?.required !== false, description: String(v?.description ?? "") });
88
+ }
89
+ return out.sort((a, b) => a.name.localeCompare(b.name));
90
+ }
91
+
92
+ // ---- the names-only env contract ---------------------------------------------
93
+
94
+ // Render a deterministic .env.example. Every variable line is exactly `NAME=`
95
+ // with nothing after the equals sign. The security invariant (no value is ever
96
+ // written) is locked by a unit test that matches `^[A-Z0-9_]+=$` on every
97
+ // non-comment, non-blank line.
98
+ export function renderEnvExample(envVars: EnvVarSpec[]): string {
99
+ const vars = dedupeSortEnv(envVars);
100
+ const lines: string[] = [
101
+ "# Environment variables for this project.",
102
+ "# Names only. Real values live in your host environment (the Netlify UI, or netlify env:set), never in this file.",
103
+ "# Copy this file to .env for local work and fill the values there. .env is gitignored; this .env.example is committed so the contract travels with the repo.",
104
+ ];
105
+ if (vars.length === 0) {
106
+ lines.push("", "# No backend environment variables were declared for this build.");
107
+ return lines.join("\n") + "\n";
108
+ }
109
+ for (const v of vars) {
110
+ const note = v.description.trim() ? v.description.trim() : "Set this value in your host environment.";
111
+ lines.push("", `# ${note} (${v.required ? "required" : "optional"})`, `${v.name}=`);
112
+ }
113
+ return lines.join("\n") + "\n";
114
+ }
115
+
116
+ // ---- the deploy guide --------------------------------------------------------
117
+
118
+ export interface BackendEndpoint {
119
+ name: string; // function file name without extension, glossary-named
120
+ rid: string; // the backend requirement id it implements
121
+ summary: string; // one line of what it does
122
+ }
123
+
124
+ export interface BackendGuideInput {
125
+ brandName?: string;
126
+ backendKind?: BackendKind;
127
+ functionsDir?: string;
128
+ runtime?: string;
129
+ database?: DatabaseSpec | null;
130
+ envVars?: EnvVarSpec[];
131
+ endpoints?: BackendEndpoint[];
132
+ }
133
+
134
+ const cell = (s: string): string => String(s ?? "").replace(/\|/g, "\\|").replace(/\n+/g, " ").trim();
135
+
136
+ // Render a deterministic BACKEND.md for the Neon-on-Netlify path. The agent
137
+ // supplies the endpoint and env lists; the prose, section order, and the honest
138
+ // "deploy automation is a follow-up" status are fixed and dash-free.
139
+ export function renderBackendGuide(input: BackendGuideInput): string {
140
+ const brand = (input.brandName || "This project").trim() || "This project";
141
+ const functionsDir = (input.functionsDir || "netlify/functions").trim() || "netlify/functions";
142
+ const db = input.database ?? NEON_DEFAULT_DB;
143
+ const envVars = dedupeSortEnv(input.envVars || []);
144
+ const endpoints = [...(input.endpoints || [])].sort(
145
+ (a, b) => String(a.rid).localeCompare(String(b.rid)) || String(a.name).localeCompare(String(b.name)),
146
+ );
147
+
148
+ const envRows =
149
+ envVars.length > 0
150
+ ? envVars
151
+ .map((v) => `| \`${v.name}\` | ${v.required ? "yes" : "no"} | ${cell(v.description) || "Set in your host environment."} |`)
152
+ .join("\n")
153
+ : `| \`${db.envVar}\` | yes | ${db.provider === "neon" ? "Neon Postgres connection string" : "Database connection string"} |`;
154
+
155
+ const fnRows =
156
+ endpoints.length > 0
157
+ ? endpoints.map((e) => `| \`${cell(e.name)}\` | ${cell(e.rid)} | ${cell(e.summary) || "See the function source."} |`).join("\n")
158
+ : "| (none emitted) | n/a | This build declared a backend but wrote no functions; see DECISIONS.md. |";
159
+
160
+ return `# Backend deploy guide
161
+
162
+ ${brand} needs a server side, so the build emitted a backend alongside the static front end. This guide covers what is in the repo, the honest status of automated backend deploy, and the one-time manual steps to take it live yourself.
163
+
164
+ ## What is in the repo
165
+
166
+ - \`${functionsDir}/\`: one serverless function per backend requirement (the table further down maps each to its R-ID). The front end calls a function at \`/.netlify/functions/<name>\`.
167
+ - \`${db.schemaFile}\`: the database schema. Tables and columns use the project glossary terms. Apply it once against your database.
168
+ - \`.env.example\`: the environment variable names this backend reads. Names only, no values. Copy it to \`.env\` for local work and set real values in your host environment.
169
+ - \`netlify.toml\`: declares the functions directory (\`[functions] directory = "${functionsDir}"\`) so Netlify bundles and serves the functions next to the static publish directory.
170
+
171
+ ## Status: automated backend deploy is a follow-up
172
+
173
+ \`tl deploy\` publishes the static front end today. It does not yet provision the database or ship the functions for you; that automation is the next piece of work. Until it lands, do the steps below once. The front end deploys and works now; the functions go live when you complete these steps.
174
+
175
+ ## 1. Provision the database
176
+
177
+ The documented default is Neon Postgres. Netlify DB is managed Neon, so this is the Netlify native choice as well as a portable one.
178
+
179
+ 1. Create a Neon project at neon.tech, or run \`netlify db init\` to provision managed Neon from your own Netlify account.
180
+ 2. Copy the connection string. It looks like \`postgresql://USER:PASSWORD@HOST/DB?sslmode=require\`.
181
+ 3. Apply the schema once: \`psql "$${db.envVar}" -f ${db.schemaFile}\`.
182
+
183
+ When you provision managed Neon through Netlify, Netlify sets \`NETLIFY_DATABASE_URL\` for you. The code reads \`${db.envVar}\` as the portable name, so set that (or map one to the other) in the next step.
184
+
185
+ To use a different Postgres provider, point \`${db.envVar}\` at it instead. Any standard Postgres works with the Neon serverless driver, so this is not locked to one vendor.
186
+
187
+ ## 2. Set the environment variables
188
+
189
+ Set these in your host environment (the Netlify UI under Site settings, Environment variables, or \`netlify env:set NAME value\`). Never commit real values; \`.env.example\` carries names only.
190
+
191
+ | Variable | Required | Purpose |
192
+ | --- | --- | --- |
193
+ ${envRows}
194
+
195
+ ## 3. The functions
196
+
197
+ Each backend requirement maps to one function. The front end reaches it at \`/.netlify/functions/<name>\`.
198
+
199
+ | Function | Requirement | Does |
200
+ | --- | --- | --- |
201
+ ${fnRows}
202
+
203
+ ## 4. Deploy with the functions present
204
+
205
+ 1. Confirm \`netlify.toml\` declares the functions directory.
206
+ 2. Set the environment variables from step 2 on the target site.
207
+ 3. From the project root, with the functions in place, run \`netlify deploy --prod\`. (Once the backend deploy automation lands, \`tl deploy\` will do this for you.)
208
+
209
+ \`netlify deploy\` ships both the static publish directory and the functions in one go. After it completes, call a function once to confirm it responds, then use the live site.
210
+ `;
211
+ }
212
+
213
+ // ---- defensive normalizer ----------------------------------------------------
214
+
215
+ function normalizeDatabase(raw: unknown): DatabaseSpec | null {
216
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
217
+ const r = raw as Record<string, unknown>;
218
+ const str = (v: unknown, fb: string): string => (typeof v === "string" && v.trim() ? v.trim() : fb);
219
+ return {
220
+ provider: str(r["provider"], "neon"),
221
+ schemaFile: str(r["schemaFile"], "schema.sql"),
222
+ envVar: str(r["envVar"], "DATABASE_URL"),
223
+ };
224
+ }
225
+
226
+ function normalizeEnvVars(raw: unknown): EnvVarSpec[] {
227
+ if (!Array.isArray(raw)) return [];
228
+ const seen = new Set<string>();
229
+ const out: EnvVarSpec[] = [];
230
+ for (const item of raw) {
231
+ if (!item || typeof item !== "object") continue;
232
+ const r = item as Record<string, unknown>;
233
+ const name = typeof r["name"] === "string" ? r["name"].trim() : "";
234
+ if (!name) continue; // drop nameless vars
235
+ if (seen.has(name)) continue; // dedupe
236
+ seen.add(name);
237
+ out.push({
238
+ name,
239
+ required: typeof r["required"] === "boolean" ? r["required"] : true,
240
+ description: typeof r["description"] === "string" ? r["description"] : "",
241
+ });
242
+ }
243
+ return out;
244
+ }
245
+
246
+ // Coerce a hand-written backend block (the agent fills it into build.json) into
247
+ // a clean BackendMeta, or null when there is nothing recognizable. Mirrors the
248
+ // progress.ts normalizers: forgiving on input, strict on output.
249
+ export function normalizeBackendMeta(raw: unknown): BackendMeta | null {
250
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
251
+ const r = raw as Record<string, unknown>;
252
+
253
+ const kindRaw = r["backendKind"];
254
+ const kind: BackendKind = kindRaw === "server" ? "server" : kindRaw === "serverless" ? "serverless" : null;
255
+ const envVars = normalizeEnvVars(r["envVars"]);
256
+ const database = normalizeDatabase(r["database"]);
257
+ const hasFunctionsDir = typeof r["functionsDir"] === "string" && (r["functionsDir"] as string).trim().length > 0;
258
+
259
+ // Nothing the deploy step could act on: treat as no backend.
260
+ if (kind === null && envVars.length === 0 && database === null && !hasFunctionsDir) return null;
261
+
262
+ const str = (v: unknown, fb: string): string => (typeof v === "string" && v.trim() ? v.trim() : fb);
263
+ return {
264
+ backendKind: kind ?? "serverless",
265
+ functionsDir: str(r["functionsDir"], "netlify/functions"),
266
+ runtime: str(r["runtime"], "nodejs20.x"),
267
+ nodeVersion: str(r["nodeVersion"], "20"),
268
+ envVars,
269
+ database,
270
+ guide: str(r["guide"], "BACKEND.md"),
271
+ };
272
+ }
package/core/deploy-io.ts CHANGED
@@ -257,9 +257,22 @@ export async function runDeploy(opts: DeployRunOptions, ctx: { deployedAt: strin
257
257
  return { ok: false, message: `Publish dir ${publishDirAbs} is empty - nothing to deploy.`, details: {} };
258
258
  }
259
259
 
260
+ // build.json may predate the backend block, so read it defensively.
261
+ const backend = manifest.backend ?? null;
260
262
  const backendWarn = manifest.hasBackend
261
- ? ` WARNING: build.json says hasBackend:true${manifest.backendNote ? ` (${manifest.backendNote})` : ""}; ` +
262
- `this static deploy publishes only the front end - the server part needs serverless functions or a separate host.`
263
+ ? (() => {
264
+ const guide = backend?.guide || "BACKEND.md";
265
+ const dbEnv = backend?.database?.envVar || "DATABASE_URL";
266
+ const names = (backend?.envVars || []).map((v) => v.name).filter(Boolean);
267
+ const others = names.filter((n) => n !== dbEnv);
268
+ const envList = others.length ? `${dbEnv} plus ${others.join(", ")}` : dbEnv;
269
+ return (
270
+ `\n\nStatic deploy: the front end goes live now. This project also has a backend ` +
271
+ `(build.json hasBackend is true${manifest.backendNote ? `: ${manifest.backendNote}` : ""}). ` +
272
+ `The backend artifact (serverless functions, schema.sql, a names-only .env.example) is built but not auto-deployed yet; backend deploy automation is a follow-up. ` +
273
+ `To run it yourself, follow ${guide}: provision Neon Postgres, set ${envList} in your host environment, then run netlify deploy with the functions present.`
274
+ );
275
+ })()
263
276
  : "";
264
277
 
265
278
  const token = process.env.NETLIFY_AUTH_TOKEN || process.env.NETLIFY_TOKEN || "";
@@ -318,7 +331,8 @@ export async function runDeploy(opts: DeployRunOptions, ctx: { deployedAt: strin
318
331
  deployRecord({
319
332
  deployedAt: ctx.deployedAt, mode: owned ? "cli" : "anonymous", publishDir: manifest.publishDir, fileCount,
320
333
  url, adminUrl: null, claimUrl, siteId: null, deployId: null,
321
- hasBackend: manifest.hasBackend, backendNote: manifest.backendNote, buildProducer: manifest.producer, stateFile,
334
+ hasBackend: manifest.hasBackend, backendNote: manifest.backendNote, backendKind: backend?.backendKind ?? null,
335
+ buildProducer: manifest.producer, stateFile,
322
336
  }),
323
337
  );
324
338
  // If anonymity was explicitly asked for but the CLI is logged in, it went
@@ -349,7 +363,8 @@ export async function runDeploy(opts: DeployRunOptions, ctx: { deployedAt: strin
349
363
  deployRecord({
350
364
  deployedAt: ctx.deployedAt, mode: "token", publishDir: manifest.publishDir, fileCount,
351
365
  url: r.url || null, adminUrl: r.adminUrl || null, claimUrl: null, siteId: r.siteId, deployId: r.deployId,
352
- hasBackend: manifest.hasBackend, backendNote: manifest.backendNote, buildProducer: manifest.producer, stateFile,
366
+ hasBackend: manifest.hasBackend, backendNote: manifest.backendNote, backendKind: backend?.backendKind ?? null,
367
+ buildProducer: manifest.producer, stateFile,
353
368
  }),
354
369
  );
355
370
  return {
package/core/deploy.ts CHANGED
@@ -10,6 +10,7 @@
10
10
  // import and it is pure (content in, hex out).
11
11
 
12
12
  import { createHash } from "node:crypto";
13
+ import type { BackendKind } from "./backend.ts";
13
14
 
14
15
  export const sha1Hex = (data: Buffer | string): string =>
15
16
  createHash("sha1").update(data).digest("hex");
@@ -106,6 +107,10 @@ export interface DeployRecord {
106
107
  deployId: string | null;
107
108
  hasBackend: boolean;
108
109
  backendNote: string | null;
110
+ // Provenance of the backend payload from build.json (null for a static build).
111
+ // The static deploy does not ship a backend; this just records what the build
112
+ // declared, for the deploy-automation follow-up to read.
113
+ backendKind?: BackendKind | null;
109
114
  buildProducer: "agent" | "scaffold" | null;
110
115
  stateFile: string | null;
111
116
  }
package/core/index.ts CHANGED
@@ -15,6 +15,7 @@ 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 "./backend.ts";
18
19
  export * from "./scaffold.ts";
19
20
  export * from "./scaffold-io.ts";
20
21
  export * from "./deploy.ts";
package/core/scaffold.ts CHANGED
@@ -11,6 +11,7 @@
11
11
  // so this emits plain HTML. Pure: no fs, no Date - the caller supplies builtAt.
12
12
 
13
13
  import type { ProgressState } from "./progress.ts";
14
+ import type { BackendMeta } from "./backend.ts";
14
15
 
15
16
  export interface StarterSiteSpec {
16
17
  brandName: string;
@@ -45,6 +46,11 @@ export interface BuildManifest {
45
46
  stack: string;
46
47
  hasBackend: boolean;
47
48
  backendNote: string | null;
49
+ // The structured backend payload, present only when the build emitted a real
50
+ // backend (hasBackend true). Optional + nullable so existing static build.json
51
+ // files round-trip and the deploy reader tolerates its absence. The deploy
52
+ // automation follow-up consumes it; today only deploy messaging reads it.
53
+ backend?: BackendMeta | null;
48
54
  buildCommand: string | null;
49
55
  installCommand: string | null;
50
56
  nodeVersion: string;
@@ -264,6 +270,7 @@ export function scaffoldManifest(
264
270
  stack: "static",
265
271
  hasBackend: false,
266
272
  backendNote: null,
273
+ backend: null,
267
274
  buildCommand: null,
268
275
  installCommand: null,
269
276
  nodeVersion: "20",
package/dist/tl.js CHANGED
@@ -624,6 +624,7 @@ function scaffoldManifest(publishDir, builtAt, provenance) {
624
624
  stack: "static",
625
625
  hasBackend: false,
626
626
  backendNote: null,
627
+ backend: null,
627
628
  buildCommand: null,
628
629
  installCommand: null,
629
630
  nodeVersion: "20",
@@ -867,7 +868,17 @@ async function runDeploy(opts, ctx) {
867
868
  if (fileCount === 0) {
868
869
  return { ok: false, message: `Publish dir ${publishDirAbs} is empty - nothing to deploy.`, details: {} };
869
870
  }
870
- const backendWarn = manifest.hasBackend ? ` WARNING: build.json says hasBackend:true${manifest.backendNote ? ` (${manifest.backendNote})` : ""}; this static deploy publishes only the front end - the server part needs serverless functions or a separate host.` : "";
871
+ const backend = manifest.backend ?? null;
872
+ const backendWarn = manifest.hasBackend ? (() => {
873
+ const guide = backend?.guide || "BACKEND.md";
874
+ const dbEnv = backend?.database?.envVar || "DATABASE_URL";
875
+ const names = (backend?.envVars || []).map((v) => v.name).filter(Boolean);
876
+ const others = names.filter((n) => n !== dbEnv);
877
+ const envList = others.length ? `${dbEnv} plus ${others.join(", ")}` : dbEnv;
878
+ return `
879
+
880
+ Static deploy: the front end goes live now. This project also has a backend (build.json hasBackend is true${manifest.backendNote ? `: ${manifest.backendNote}` : ""}). The backend artifact (serverless functions, schema.sql, a names-only .env.example) is built but not auto-deployed yet; backend deploy automation is a follow-up. To run it yourself, follow ${guide}: provision Neon Postgres, set ${envList} in your host environment, then run netlify deploy with the functions present.`;
881
+ })() : "";
871
882
  const token = process.env.NETLIFY_AUTH_TOKEN || process.env.NETLIFY_TOKEN || "";
872
883
  const writeRecord = (rec) => {
873
884
  const recPath = join3(dirname3(stateFile), "deploy.json");
@@ -921,6 +932,7 @@ async function runDeploy(opts, ctx) {
921
932
  deployId: null,
922
933
  hasBackend: manifest.hasBackend,
923
934
  backendNote: manifest.backendNote,
935
+ backendKind: backend?.backendKind ?? null,
924
936
  buildProducer: manifest.producer,
925
937
  stateFile
926
938
  })
@@ -957,6 +969,7 @@ Recorded ${recPath}.${backendWarn}` + (!url || !claimUrl ? `
957
969
  deployId: r.deployId,
958
970
  hasBackend: manifest.hasBackend,
959
971
  backendNote: manifest.backendNote,
972
+ backendKind: backend?.backendKind ?? null,
960
973
  buildProducer: manifest.producer,
961
974
  stateFile
962
975
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hobocode/thought-layer",
3
- "version": "0.4.1",
3
+ "version": "0.5.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,12 +64,14 @@
64
64
  "core/stage-map.ts",
65
65
  "core/state-file.ts",
66
66
  "core/state-ops.ts",
67
+ "core/backend.ts",
67
68
  "core/scaffold.ts",
68
69
  "core/scaffold-io.ts",
69
70
  "core/deploy.ts",
70
71
  "core/deploy-io.ts",
71
72
  "dist",
72
73
  "README.md",
74
+ "SECURITY.md",
73
75
  "LICENSE"
74
76
  ]
75
77
  }
@@ -2,6 +2,6 @@ Apply the **thought-layer-build** skill. Build the hardened PRD into a static-fi
2
2
 
3
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
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`.
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; when the three-question backend test shows a requirement genuinely needs a server, build the real backend too: serverless functions under `netlify/functions/` (one per backend R-ID, glossary-named), a `schema.sql`, a names-only `.env.example` (with `!.env.example` un-ignored in `.gitignore`), an updated `netlify.toml`, and a `BACKEND.md` guide, recorded in the manifest's `backend` block. Be honest that backend deploy automation is a follow-up, so `tl deploy` still ships the static front end for now. Verify the build runs, and leave `.thought-layer/build.json` + `DECISIONS.md` + `TRACEABILITY.md`.
6
6
 
7
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.
@@ -2,6 +2,6 @@ Apply the **thought-layer-deploy** skill. Take the built site live to a URL I ow
2
2
 
3
3
  Read `.thought-layer/build.json` (next to the state file; honor `--path` / `THOUGHT_LAYER_STATE` if a named file is in use) for the publish directory and entry. If there is no `build.json`, say so and point me to `/tl-build` (or the `tl_scaffold` tool) rather than guessing - the build has to run first.
4
4
 
5
- Default to a dry run first so I can see exactly which files would ship and where. Then deploy: if `NETLIFY_AUTH_TOKEN` is set, deploy into my own Netlify account (owned immediately); otherwise delegate to my Netlify CLI - logged in it creates a site in my account, logged out it deploys anonymously with a one-hour claim link. Read the token only from the environment, never ask me to paste it. If `build.json` says `hasBackend: true`, warn me clearly that this static deploy publishes only the front end.
5
+ Default to a dry run first so I can see exactly which files would ship and where. Then deploy: if `NETLIFY_AUTH_TOKEN` is set, deploy into my own Netlify account (owned immediately); otherwise delegate to my Netlify CLI - logged in it creates a site in my account, logged out it deploys anonymously with a one-hour claim link. Read the token only from the environment, never ask me to paste it. If `build.json` says `hasBackend: true`, tell me plainly that this static deploy ships only the front end for now (backend deploy automation is a follow-up), and point me at `BACKEND.md` for the one-time steps to run the backend (provision Neon, set `DATABASE_URL`, then `netlify deploy` with the functions present).
6
6
 
7
7
  After it is live, tell me the URL and (anonymous only) the claim link, and that a `.thought-layer/deploy.json` record was written.
@@ -1,6 +1,6 @@
1
1
  ---
2
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."
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. When the spec genuinely requires a server, it also emits a real backend (serverless functions under netlify/functions/, a schema.sql, a names-only .env.example, an updated netlify.toml, and a BACKEND.md guide) and records it in the manifest, while staying honest that backend deploy automation is a follow-up so the default deploy path stays 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
4
  ---
5
5
 
6
6
  # Build it: the hardened PRD becomes a deploy-ready artifact
@@ -49,7 +49,17 @@ Default to a self-contained static site or a Vite/Astro/static build whose outpu
49
49
 
50
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
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.
52
+ **When a backend is genuinely required, build it for real (do not just warn).** Build the static front end as above, set `hasBackend: true` and a one-line `backendNote`, and emit a coherent, buildable serverless backend alongside it:
53
+
54
+ - **Serverless functions, one per backend R-ID**, under `netlify/functions/`. Name each file in the ubiquitous language (the glossary term for what it does, e.g. `netlify/functions/dispatch.ts`), open it with a comment naming the R-ID it implements, and keep it inside the out-of-scope boundary. Each function reads its inputs, talks to the database, and returns JSON. Give the front end a clear seam: it calls the function at `fetch('/.netlify/functions/<name>')`, never a hardcoded host.
55
+ - **A `schema.sql`** at the project root, derived from the PRD data requirements and the domain entities. Name tables and columns in the glossary terms (no synonyms), and keep it idempotent where you can (`create table if not exists ...`).
56
+ - **Neon Postgres by default.** The functions reach the database through the Neon serverless driver (`@neondatabase/serverless`, added to the product's `package.json`, never the kit's) and read the connection string from `DATABASE_URL` (Netlify sets `NETLIFY_DATABASE_URL` when you provision managed Neon, so read `DATABASE_URL` and map it). Neon is the single documented default and is overridable to any Postgres by pointing `DATABASE_URL` elsewhere; state that in `BACKEND.md` and do not invent a second provider.
57
+ - **A names-only `.env.example`** at the project root listing every variable the backend reads (`DATABASE_URL` plus any others), each as a bare `NAME=` under a one-line comment. Never write a real value; real values live only in the host environment.
58
+ - **An updated `netlify.toml`.** Extend the existing publish + redirect block (do not replace it) with a `[functions]` table declaring `directory = "netlify/functions"`. Keep the static publish dir and the SPA redirect intact.
59
+ - **A `BACKEND.md` deploy guide** at the project root: what is in the repo, the honest status (automated backend deploy is a follow-up, so `tl deploy` ships only the front end today), how to provision Neon, the env-var table, the function-to-R-ID table, and the manual `netlify deploy` steps. The kit's `renderBackendGuide` and `renderEnvExample` helpers (in `core/backend.ts`) produce a dash-free skeleton if you have the core available; otherwise write the same content by hand.
60
+ - **A project `.gitignore`** that ignores `.env` and `.env.*` but un-ignores the contract with `!.env.example`. Without that line the env contract is silently un-committable, and the deploy follow-up cannot read it.
61
+
62
+ Then record the backend in the manifest's `backend` block (shape below). Do **not** attempt to deploy the backend in this step: `tl deploy` publishes the static front end, and backend deploy automation is the explicit follow-up. Say so plainly in chat and point the user at `BACKEND.md`.
53
63
 
54
64
  ## SEO and discoverability (build all of it, do not skip it)
55
65
 
@@ -67,7 +77,8 @@ Do not declare victory - check:
67
77
  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
78
  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
79
  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.
80
+ 5. **Backend check (only when `hasBackend`).** Each backend R-ID maps to a function in TRACEABILITY.md; `netlify/functions/` has one file per backend R-ID; `.env.example` is values-free (every variable line is a bare `NAME=`, never a value); `schema.sql` is non-empty; `netlify.toml` declares the functions directory; `.gitignore` has `!.env.example`. Do **not** try to run the backend or reach a database here (there is no `DATABASE_URL` in the build env); confirm the artifact is coherent and buildable, not live.
81
+ 6. **Report** what is built and what is deferred, plainly, in chat.
71
82
 
72
83
  ## Honest about being model-built
73
84
 
@@ -82,6 +93,7 @@ Write three files with your own file tools (the manifest is NOT a `tl_state` art
82
93
  { "app": "thought-layer", "kind": "build", "version": 1, "builtAt": "<ISO>",
83
94
  "producer": "agent", "publishDir": "dist", "entry": "index.html",
84
95
  "stack": "static|vite|astro|next-static|other", "hasBackend": false, "backendNote": null,
96
+ "backend": null,
85
97
  "buildCommand": null, "installCommand": null, "nodeVersion": "20",
86
98
  "provenance": { "stateFile": "<the file you read>", "prdTs": <state.prd.ts>, "grillDone": <bool>, "fromSpeedrun": <bool> },
87
99
  "requirements": { "total": 0, "built": 0, "deferred": 0, "deferredIds": [] },
@@ -90,6 +102,17 @@ Write three files with your own file tools (the manifest is NOT a `tl_state` art
90
102
  "verified": { "buildRan": true, "publishDirExists": true, "entryLoads": true, "notes": "..." } }
91
103
  ```
92
104
  `publishDir` + `entry` are load-bearing - the deploy step reads them. (The `tl_scaffold` tool writes this same manifest with `producer: "scaffold"`.)
105
+
106
+ When `hasBackend` is `true`, populate `backend` (leave it `null` for a static build). This is the forward-looking contract the backend deploy automation will consume:
107
+
108
+ ```jsonc
109
+ "backend": {
110
+ "backendKind": "serverless", "functionsDir": "netlify/functions",
111
+ "runtime": "nodejs20.x", "nodeVersion": "20",
112
+ "envVars": [{ "name": "DATABASE_URL", "required": true, "description": "Neon Postgres connection string" }],
113
+ "database": { "provider": "neon", "schemaFile": "schema.sql", "envVar": "DATABASE_URL" },
114
+ "guide": "BACKEND.md" }
115
+ ```
93
116
  - `DECISIONS.md` - every choice the spec did not pin down, one line each, with the reason.
94
117
  - `TRACEABILITY.md` - the R-ID map. (`SEO.md` comes from the SEO step.)
95
118
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: thought-layer-deploy
3
- description: "Take the built site live to a user-owned URL with no lock-in, the last step after the build. Reads .thought-layer/build.json (the publish dir + entry) next to the state file, then deploys to Netlify by one of two BYOK models: with NETLIFY_AUTH_TOKEN set it deploys into the user's OWN account via the file-digest API (owned immediately, no claim), and with no token it delegates to the user's Netlify CLI (logged in: a new site in their account; logged out: an anonymous, claimable URL with a one-hour claim link). Static-first: if build.json says hasBackend it warns that only the front end ships this way. Prefers the deploy tool (Pi) or the tl deploy CLI (any shell agent) so the deploy is one mechanical, honest step, never hand-rolled. Run it after thought-layer-build (or tl_scaffold) has produced build.json."
3
+ description: "Take the built site live to a user-owned URL with no lock-in, the last step after the build. Reads .thought-layer/build.json (the publish dir + entry) next to the state file, then deploys to Netlify by one of two BYOK models: with NETLIFY_AUTH_TOKEN set it deploys into the user's OWN account via the file-digest API (owned immediately, no claim), and with no token it delegates to the user's Netlify CLI (logged in: a new site in their account; logged out: an anonymous, claimable URL with a one-hour claim link). Static-first: if build.json says hasBackend, the build also emitted a backend (functions, schema.sql, a names-only .env.example, BACKEND.md), but backend deploy automation is a follow-up, so this step ships only the front end for now and points at BACKEND.md. Prefers the deploy tool (Pi) or the tl deploy CLI (any shell agent) so the deploy is one mechanical, honest step, never hand-rolled. Run it after thought-layer-build (or tl_scaffold) has produced build.json."
4
4
  ---
5
5
 
6
6
  # Deploy it: the build goes live to a URL you own
@@ -35,7 +35,7 @@ If neither a token nor a usable CLI is available, relay the tool's guidance hone
35
35
 
36
36
  ## Static-first honesty
37
37
 
38
- The default deploy publishes a **static** publish directory. If `build.json.hasBackend` is `true`, the tool warns and you must repeat it plainly: only the front end goes live this way; the server part needs serverless functions or a separate host. Do not imply a backend is running when it is not.
38
+ The default deploy publishes a **static** publish directory. If `build.json.hasBackend` is `true`, the build also emitted a real backend (serverless functions, a `schema.sql`, a names-only `.env.example`, an updated `netlify.toml`, and a `BACKEND.md` guide), but **backend deploy automation is a follow-up**, so this step ships only the front end for now. The tool warns; repeat its guidance plainly and point the user at `BACKEND.md`. To run the backend they provision Neon Postgres, set `DATABASE_URL` (and the other names from `.env.example`) in their host environment, then run `netlify deploy` with the functions present. Do not imply a backend is running when it is not.
39
39
 
40
40
  ## After it is live
41
41