@hobocode/thought-layer 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hobocode LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # The Thought Layer Kit
2
+
3
+ Rigor for building. AI made building cheap. It did not make **knowing what to build** cheap, and it did not make a confident, defensible plan cheap. This kit puts that rigor inside the agent you already use, then carries it toward a live thing you own.
4
+
5
+ The loop it is building toward:
6
+
7
+ > **validate the idea, grill it into a buildable spec, build it, deploy it.** One agent. Your own key. Nothing phones home.
8
+
9
+ This is open source and BYOK by design. The point is to help people build real things instead of confident slop, not to sell you a platform.
10
+
11
+ ## What is here
12
+
13
+ **The rigor, as portable [Agent Skills](https://www.anthropic.com/news/skills)** (work in any agent that reads the format):
14
+
15
+ - **thought-layer-framework.** The backbone. Walks a founder through the full staged framework in order: validate the idea (what it is, domain knowledge, validation, market selection, the pitch), make the business model real (time, costs, scale, pricing, the model, acquisition, relationships, support), then design (PRD draft, then grill) last. It evaluates each stage at its own altitude, so a one-line idea is never audited for implementation details.
16
+ - **thought-layer-panel.** Pressure-test the answer to one stage with an adversarial panel (red team, domain expert, skeptical investor), at that stage's altitude. Confidence score, letter grade, at most three stage-appropriate fixes; later-stage concerns get parked, not penalized.
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
+ - **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
+ - **thought-layer-naming.** Name the thing, with rationale and domain-ready slugs.
20
+
21
+ **A Pi package** that adds, on top of the skills:
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.
25
+
26
+ ## Install
27
+
28
+ ### Pi
29
+
30
+ ```bash
31
+ pi install npm:@hobocode/thought-layer
32
+ # or, before it is published:
33
+ pi install git:github.com/hobocode-ofc/thought-layer-kit
34
+ ```
35
+
36
+ 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`.
37
+
38
+ ### Claude Code (or any agent that reads the Agent Skills format)
39
+
40
+ Copy each skill folder into `~/.claude/skills/`:
41
+
42
+ ```bash
43
+ cp -r skills/* ~/.claude/skills/
44
+ ```
45
+
46
+ The skills work as-is; the Pi-specific tools and slash commands are Pi only. Other agents adopt the `SKILL.md` format with minor adaptation.
47
+
48
+ ## How to use it
49
+
50
+ Run the whole framework with `/tl`. It walks the stages in order and does not skip ahead:
51
+
52
+ 1. **Validate the idea:** what it is, your domain knowledge, validation (will anyone pay), market selection, the 30-second pitch. The panel judges each at the idea's altitude. It will not pressure-test how the thing is built yet.
53
+ 2. **Make the model real:** time, costs, scale, pricing, the business model and its numbers (via `tl_project`), acquisition, relationships, support. This is where unit economics and operational logistics get scrutinized.
54
+ 3. **Design it (last):** `/tl-prd` drafts the PRD (with a first-cut glossary and requirements); `/tl-grill` then grills that draft against the domain until it is build-ready. Every "how will it actually work" concern parked during validation gets resolved here.
55
+
56
+ Each stage clears at confidence 0.85 or when you set it aside (open items carry forward as to-dos). You can also run a single stage directly: `/tl-panel`, `/tl-grill`, `/tl-prd`, `/tl-naming`.
57
+
58
+ To check domains live, set a RapidAPI key in your environment (`THOUGHT_LAYER_DOMAIN_KEY` or `RAPIDAPI_KEY`). With no key, naming links out to a domain search instead of calling out.
59
+
60
+ The hosted version of the rigor lives at [weareallproductmanagersnow.com](https://weareallproductmanagersnow.com) if you would rather not install anything.
61
+
62
+ ## Roadmap
63
+
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.
66
+ - **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
+ ## Notes for contributors
69
+
70
+ - The deterministic engine in `core/` is TypeScript, with `vitest` tests (`npm test`) and a strict `tsc --noEmit` typecheck. It is the single source of truth for scoring, domain checks, and the projection model.
71
+ - This is a TypeScript-source package: relative imports carry `.ts` extensions so Pi's loader and Vite resolve them directly. It is meant to be consumed by TS-aware tooling, not a plain Node `require`.
72
+ - **Iterating on skills in Pi:** `pi update` syncs files to disk but a running Pi session keeps the skill registry it built at startup — it does not hot-reload. After adding or editing a skill or prompt, **restart Pi** (or run `/reload` if your build supports it) to pick up the change. Symptom of a stale session: a newly added skill is missing from the picker, or a skill shows an outdated description.
73
+
74
+ ## Acknowledgments
75
+
76
+ The Grill skill's interview technique — relentless, one question at a time, sharpening the domain glossary as it goes — is inspired by Matt Pocock's [`grill-with-docs`](https://github.com/mattpocock/skills/blob/main/skills/engineering/grill-with-docs/SKILL.md) (MIT, © Matt Pocock). His grills an architecture plan against existing domain docs; this kit adapts the technique to grill a draft PRD against the domain, hardening its glossary and requirements inline.
77
+
78
+ ## License
79
+
80
+ MIT. Copyright Hobocode LLC.
@@ -0,0 +1,67 @@
1
+ // Domain availability via the Domains API on RapidAPI. The key is the user's
2
+ // own (BYOK); it is sent only to this host. With no key, checkDomains returns
3
+ // null and the caller falls back to a registrar search link, so nothing leaves
4
+ // the machine uninvited. Ported from the web app's src/lib/domains.js.
5
+
6
+ export const DOMAIN_HOST = "domains-api.p.rapidapi.com";
7
+ export const DOMAIN_TLDS = ["com", "io", "app", "co"] as const;
8
+
9
+ export interface DomainResult {
10
+ domain: string;
11
+ status: string;
12
+ available: boolean;
13
+ error?: boolean;
14
+ }
15
+
16
+ export interface CheckOptions {
17
+ signal?: AbortSignal;
18
+ tlds?: readonly string[];
19
+ }
20
+
21
+ // A slug -> the candidate domains we test for it.
22
+ export function domainsForSlug(slug: string, tlds: readonly string[] = DOMAIN_TLDS): string[] {
23
+ const base = String(slug || "").toLowerCase().replace(/[^a-z0-9-]/g, "");
24
+ if (!base) return [];
25
+ return tlds.map((t) => `${base}.${t}`);
26
+ }
27
+
28
+ // The API returns availability as "available" | "registered" (and a few others
29
+ // like "reserved"). Only an outright "available" counts as registrable.
30
+ export function isAvailable(availability: string | null | undefined): boolean {
31
+ return String(availability || "").toLowerCase() === "available";
32
+ }
33
+
34
+ // Returns null when there is no key (caller shows registrar links instead),
35
+ // otherwise [{ domain, status, available, error? }]. The API takes a single
36
+ // domain per request, so we fan out one request per candidate TLD in parallel.
37
+ // Each lookup is resilient: one TLD failing (some return 500 for certain names)
38
+ // marks just that chip as a failed check, it does not sink the whole batch.
39
+ export async function checkDomains(
40
+ slug: string,
41
+ domainKey: string | null | undefined,
42
+ { signal, tlds }: CheckOptions = {},
43
+ ): Promise<DomainResult[] | null> {
44
+ if (!domainKey) return null;
45
+ const domains = domainsForSlug(slug, tlds);
46
+ if (domains.length === 0) return [];
47
+ return Promise.all(
48
+ domains.map(async (domain): Promise<DomainResult> => {
49
+ try {
50
+ const res = await fetch(`https://${DOMAIN_HOST}/domains/${encodeURIComponent(domain)}`, {
51
+ signal,
52
+ headers: { "x-rapidapi-key": domainKey, "x-rapidapi-host": DOMAIN_HOST },
53
+ });
54
+ if (!res.ok) return { domain, status: "error", available: false, error: true };
55
+ const data = (await res.json()) as { availability?: string };
56
+ return { domain, status: data.availability || "unknown", available: isAvailable(data.availability) };
57
+ } catch {
58
+ return { domain, status: "error", available: false, error: true };
59
+ }
60
+ }),
61
+ );
62
+ }
63
+
64
+ // Outbound search used when no domain key is set (privacy-clean: a click, not a call).
65
+ export function registrarSearchUrl(slug: string): string {
66
+ return `https://instantdomainsearch.com/?q=${encodeURIComponent(String(slug || "").toLowerCase())}`;
67
+ }
package/core/index.ts ADDED
@@ -0,0 +1,12 @@
1
+ // The deterministic core of The Thought Layer: scoring, domain checks, and the
2
+ // numeric projection model. No model calls, no side effects beyond the domain
3
+ // fetch. This is the single source of truth the Pi tools and any other consumer
4
+ // share, so the math is exact and never re-derived by an LLM.
5
+ //
6
+ // Relative imports carry explicit .ts extensions so Pi's jiti loader and Vite
7
+ // resolve them without guesswork. This is a TypeScript-source package consumed
8
+ // by TS-aware tooling (Pi/jiti, Vite), not by a plain Node require.
9
+
10
+ export * from "./scoring.ts";
11
+ export * from "./domains.ts";
12
+ export * from "./model.ts";
package/core/model.ts ADDED
@@ -0,0 +1,196 @@
1
+ // Deterministic monthly projection engine. The AI proposes assumptions; all
2
+ // arithmetic happens here. Ported verbatim-in-behavior from the web app's
3
+ // src/lib/model.js (with explicit types).
4
+
5
+ export interface Party {
6
+ id: string;
7
+ name?: string;
8
+ role?: string;
9
+ startingCount?: number | string;
10
+ monthlyNewBase?: number | string;
11
+ monthlyNewGrowthPct?: number | string;
12
+ monthlyChurnPct?: number | string;
13
+ revenuePerUnitPerMonth?: number | string;
14
+ variableCostPerUnitPerMonth?: number | string;
15
+ cacPerUnit?: number | string;
16
+ notes?: string;
17
+ }
18
+
19
+ export interface FixedCost {
20
+ id: string;
21
+ name?: string;
22
+ monthlyAmount?: number | string;
23
+ startMonth?: number | string;
24
+ notes?: string;
25
+ }
26
+
27
+ export interface OneTimeCost {
28
+ id: string;
29
+ name?: string;
30
+ amount?: number | string;
31
+ month?: number | string;
32
+ notes?: string;
33
+ }
34
+
35
+ export interface Assumptions {
36
+ parties: Party[];
37
+ fixedCosts?: FixedCost[];
38
+ oneTimeCosts?: OneTimeCost[];
39
+ horizonMonths?: number;
40
+ currency?: string;
41
+ narrative?: string;
42
+ }
43
+
44
+ export interface PartyRow {
45
+ count: number;
46
+ newUnits: number;
47
+ churned: number;
48
+ revenue: number;
49
+ varCost: number;
50
+ cac: number;
51
+ }
52
+
53
+ export interface ProjectionRow {
54
+ month: number;
55
+ parties: Record<string, PartyRow>;
56
+ revenue: number;
57
+ variableCost: number;
58
+ cacSpend: number;
59
+ fixedCost: number;
60
+ oneTimeCost: number;
61
+ totalCost: number;
62
+ grossProfit: number;
63
+ netProfit: number;
64
+ cumulative: number;
65
+ }
66
+
67
+ export interface ProjectionSummary {
68
+ horizon: number;
69
+ breakEvenMonth: number | null;
70
+ cumBreakEvenMonth: number | null;
71
+ year1Revenue: number;
72
+ year1Net: number;
73
+ totalRevenue: number;
74
+ totalNet: number;
75
+ maxDrawdown: number;
76
+ endingMRR: number;
77
+ endingCounts: Record<string, number>;
78
+ }
79
+
80
+ export interface Projection {
81
+ rows: ProjectionRow[];
82
+ summary: ProjectionSummary;
83
+ }
84
+
85
+ function num(v: unknown, fallback = 0): number {
86
+ const n = Number(v);
87
+ return Number.isFinite(n) ? n : fallback;
88
+ }
89
+ function sum(arr: number[]): number {
90
+ return arr.reduce((a, b) => a + b, 0);
91
+ }
92
+
93
+ export function computeProjection(assumptions: Assumptions | null | undefined): Projection | null {
94
+ if (!assumptions?.parties?.length) return null;
95
+ const horizon = Math.min(Math.max(assumptions.horizonMonths || 36, 6), 120);
96
+ const parties = assumptions.parties;
97
+ const fixedCosts = assumptions.fixedCosts || [];
98
+ const oneTimeCosts = assumptions.oneTimeCosts || [];
99
+
100
+ const counts: Record<string, number> = {};
101
+ parties.forEach((p) => {
102
+ counts[p.id] = num(p.startingCount);
103
+ });
104
+
105
+ const rows: ProjectionRow[] = [];
106
+ let cumulative = 0;
107
+ let breakEvenMonth: number | null = null;
108
+ let cumBreakEvenMonth: number | null = null;
109
+
110
+ for (let m = 1; m <= horizon; m++) {
111
+ const row: ProjectionRow = {
112
+ month: m, parties: {}, revenue: 0, variableCost: 0, cacSpend: 0, fixedCost: 0, oneTimeCost: 0,
113
+ totalCost: 0, grossProfit: 0, netProfit: 0, cumulative: 0,
114
+ };
115
+
116
+ for (const p of parties) {
117
+ const prev = counts[p.id] ?? 0;
118
+ const newUnits = num(p.monthlyNewBase) * Math.pow(1 + num(p.monthlyNewGrowthPct) / 100, m - 1);
119
+ const churned = prev * (num(p.monthlyChurnPct) / 100);
120
+ const count = Math.max(0, prev + newUnits - churned);
121
+ counts[p.id] = count;
122
+ const revenue = count * num(p.revenuePerUnitPerMonth);
123
+ const varCost = count * num(p.variableCostPerUnitPerMonth);
124
+ const cac = newUnits * num(p.cacPerUnit);
125
+ row.parties[p.id] = { count, newUnits, churned, revenue, varCost, cac };
126
+ row.revenue += revenue;
127
+ row.variableCost += varCost;
128
+ row.cacSpend += cac;
129
+ }
130
+
131
+ for (const f of fixedCosts) {
132
+ if (m >= num(f.startMonth, 1)) row.fixedCost += num(f.monthlyAmount);
133
+ }
134
+ for (const o of oneTimeCosts) {
135
+ if (num(o.month, 1) === m) row.oneTimeCost += num(o.amount);
136
+ }
137
+
138
+ row.totalCost = row.variableCost + row.cacSpend + row.fixedCost + row.oneTimeCost;
139
+ row.grossProfit = row.revenue - row.variableCost;
140
+ row.netProfit = row.revenue - row.totalCost;
141
+ cumulative += row.netProfit;
142
+ row.cumulative = cumulative;
143
+
144
+ if (breakEvenMonth === null && row.netProfit > 0) breakEvenMonth = m;
145
+ if (cumBreakEvenMonth === null && cumulative > 0) cumBreakEvenMonth = m;
146
+ rows.push(row);
147
+ }
148
+
149
+ const last = rows[rows.length - 1];
150
+ if (!last) return null;
151
+ const year1 = rows.slice(0, 12);
152
+ return {
153
+ rows,
154
+ summary: {
155
+ horizon,
156
+ breakEvenMonth,
157
+ cumBreakEvenMonth,
158
+ year1Revenue: sum(year1.map((r) => r.revenue)),
159
+ year1Net: sum(year1.map((r) => r.netProfit)),
160
+ totalRevenue: sum(rows.map((r) => r.revenue)),
161
+ totalNet: cumulative,
162
+ maxDrawdown: Math.min(...rows.map((r) => r.cumulative), 0),
163
+ endingMRR: last.revenue,
164
+ endingCounts: Object.fromEntries(parties.map((p) => [p.id, last.parties[p.id]?.count || 0])),
165
+ },
166
+ };
167
+ }
168
+
169
+ export function fmtMoney(n: number, currency = "USD"): string {
170
+ try {
171
+ return new Intl.NumberFormat("en-US", { style: "currency", currency, maximumFractionDigits: 0 }).format(n);
172
+ } catch {
173
+ return `$${Math.round(n).toLocaleString()}`;
174
+ }
175
+ }
176
+
177
+ export function fmtNum(n: number): string {
178
+ return Math.round(n).toLocaleString();
179
+ }
180
+
181
+ // Apply a benchmark suggestion path like "parties.rider.cacPerUnit" immutably.
182
+ export function applyBenchmarkPath(assumptions: Assumptions, path: string, value: number): Assumptions {
183
+ const [group, id, field] = path.split(".");
184
+ const next: Assumptions = structuredClone(assumptions);
185
+ if (group === "parties" && id && field) {
186
+ const p = next.parties.find((x) => x.id === id);
187
+ if (p && field in p) (p as unknown as Record<string, unknown>)[field] = value;
188
+ } else if (group === "fixedCosts" && id && field) {
189
+ const f = (next.fixedCosts || []).find((x) => x.id === id);
190
+ if (f) (f as unknown as Record<string, unknown>)[field] = value;
191
+ } else if (group === "oneTimeCosts" && id && field) {
192
+ const o = (next.oneTimeCosts || []).find((x) => x.id === id);
193
+ if (o) (o as unknown as Record<string, unknown>)[field] = value;
194
+ }
195
+ return next;
196
+ }
@@ -0,0 +1,56 @@
1
+ // Confidence-driven scoring. The feedback loop's goal is a confidence above
2
+ // CONFIDENCE_GOAL; below that the loop keeps going (no round cap). All the
3
+ // band/grade thresholds live here so every consumer agrees on one source of
4
+ // truth. Ported verbatim-in-behavior from the web app's src/lib/scoring.js.
5
+
6
+ export type Status = "green" | "yellow" | "red";
7
+ export type Grade = "A" | "B" | "C" | "D" | "F";
8
+
9
+ export interface FeedbackLike {
10
+ confidence?: number;
11
+ status?: Status | string | null;
12
+ }
13
+
14
+ export const CONFIDENCE_GOAL = 0.85;
15
+
16
+ // confidence (0..1) -> stoplight status. null in, null out.
17
+ export function statusFromConfidence(c: number | null | undefined): Status | null {
18
+ if (typeof c !== "number" || Number.isNaN(c)) return null;
19
+ if (c >= CONFIDENCE_GOAL) return "green";
20
+ if (c >= 0.6) return "yellow";
21
+ return "red";
22
+ }
23
+
24
+ // confidence (0..1) -> letter grade. null when there is nothing to grade.
25
+ export function gradeFromConfidence(c: number | null | undefined): Grade | null {
26
+ if (typeof c !== "number" || Number.isNaN(c)) return null;
27
+ if (c >= 0.9) return "A";
28
+ if (c >= 0.8) return "B";
29
+ if (c >= 0.7) return "C";
30
+ if (c >= 0.6) return "D";
31
+ return "F";
32
+ }
33
+
34
+ // Mean of the numeric values, ignoring nulls/NaN. null when none are numeric.
35
+ // Used to aggregate the panel's three persona confidences into one number,
36
+ // and to aggregate a section's question confidences.
37
+ export function aggregateConfidence(values: Array<number | null | undefined>): number | null {
38
+ const nums = (values || []).filter((v): v is number => typeof v === "number" && !Number.isNaN(v));
39
+ if (nums.length === 0) return null;
40
+ return nums.reduce((a, b) => a + b, 0) / nums.length;
41
+ }
42
+
43
+ // Coarse proxy confidence for a stoplight status, used for items that have a
44
+ // status but no numeric confidence, and for legacy feedback.
45
+ const STATUS_CONFIDENCE: Record<string, number> = { green: 0.9, yellow: 0.7, red: 0.4 };
46
+ export function confidenceFromStatus(status: string | null | undefined): number | null {
47
+ if (status == null) return null;
48
+ return STATUS_CONFIDENCE[status] ?? null;
49
+ }
50
+
51
+ // Pull the effective confidence off a feedback object.
52
+ export function feedbackConfidence(fb: FeedbackLike | null | undefined): number | null {
53
+ if (!fb) return null;
54
+ if (typeof fb.confidence === "number") return fb.confidence;
55
+ return confidenceFromStatus(fb.status);
56
+ }
@@ -0,0 +1,128 @@
1
+ // The Thought Layer Pi extension. It exposes the deterministic core as tools so
2
+ // the agent never has to re-derive the math: confidence scoring, domain
3
+ // availability, and the numeric projection. The methodology itself lives in the
4
+ // skills (thought-layer-panel / grill / prd / naming); this is the engine.
5
+
6
+ import type { ExtensionAPI, ToolResult } from "@earendil-works/pi-coding-agent";
7
+ import { Type } from "@sinclair/typebox";
8
+ import {
9
+ aggregateConfidence, statusFromConfidence, gradeFromConfidence,
10
+ checkDomains, registrarSearchUrl,
11
+ computeProjection, fmtMoney,
12
+ type Assumptions,
13
+ } from "../core/index.ts";
14
+
15
+ const text = (t: string, details?: Record<string, unknown>): ToolResult => ({
16
+ content: [{ type: "text", text: t }],
17
+ details: details ?? {},
18
+ });
19
+
20
+ export default function (pi: ExtensionAPI) {
21
+ // tl_score: aggregate persona confidences into a status + letter grade,
22
+ // using the exact bands from the web app (green >= 0.85, yellow >= 0.6).
23
+ pi.registerTool({
24
+ name: "tl_score",
25
+ label: "Thought Layer: score",
26
+ description:
27
+ "Aggregate one or more confidence values (0 to 1) into a stoplight status and a letter grade, using The Thought Layer's exact bands. Use after a panel evaluation to compute the verdict instead of guessing the grade.",
28
+ parameters: Type.Object({
29
+ confidences: Type.Array(Type.Number(), {
30
+ description: "Confidence values 0 to 1 (e.g. one per persona in panel mode).",
31
+ }),
32
+ }),
33
+ async execute(_id, params): Promise<ToolResult> {
34
+ const { confidences } = params as { confidences: number[] };
35
+ const confidence = aggregateConfidence(confidences);
36
+ if (confidence === null) return text("No numeric confidences provided.", { confidence: null });
37
+ const status = statusFromConfidence(confidence);
38
+ const grade = gradeFromConfidence(confidence);
39
+ return text(
40
+ `Aggregate confidence ${(confidence * 100).toFixed(0)}% -> status ${status}, grade ${grade}. ` +
41
+ `Goal is 0.85+ (green). ${status === "green" ? "Sufficient to move on." : "Keep refining or set aside with to-dos."}`,
42
+ { confidence, status, grade },
43
+ );
44
+ },
45
+ });
46
+
47
+ // tl_domains: check availability via the user's own RapidAPI key (BYOK, from
48
+ // env). With no key, return a registrar search link instead of calling out.
49
+ pi.registerTool({
50
+ name: "tl_domains",
51
+ label: "Thought Layer: domains",
52
+ description:
53
+ "Check domain availability for a name slug across common TLDs. Reads a RapidAPI key from THOUGHT_LAYER_DOMAIN_KEY or RAPIDAPI_KEY (BYOK). With no key set, returns a registrar search link rather than calling out.",
54
+ parameters: Type.Object({
55
+ slug: Type.String({ description: "Domain-ready base, lowercase, no TLD (e.g. 'acmedispatch')." }),
56
+ tlds: Type.Optional(Type.Array(Type.String(), { description: "TLDs to check; defaults to com, io, app, co." })),
57
+ }),
58
+ async execute(_id, params, signal): Promise<ToolResult> {
59
+ const { slug, tlds } = params as { slug: string; tlds?: string[] };
60
+ const key = process.env.THOUGHT_LAYER_DOMAIN_KEY || process.env.RAPIDAPI_KEY || "";
61
+ const results = await checkDomains(slug, key, { signal, tlds });
62
+ if (results === null) {
63
+ return text(
64
+ `No domain key set, so I did not call out. Search manually: ${registrarSearchUrl(slug)}`,
65
+ { hasKey: false, searchUrl: registrarSearchUrl(slug) },
66
+ );
67
+ }
68
+ const lines = results.map((r) => `${r.available ? "available" : r.error ? "check failed" : "taken"}: ${r.domain}`);
69
+ return text(`Domain availability for "${slug}":\n${lines.join("\n")}`, { results });
70
+ },
71
+ });
72
+
73
+ // tl_project: run the deterministic monthly projection from business-model
74
+ // assumptions and return the headline summary.
75
+ const PartySchema = Type.Object({
76
+ id: Type.String(),
77
+ name: Type.Optional(Type.String()),
78
+ startingCount: Type.Optional(Type.Number()),
79
+ monthlyNewBase: Type.Optional(Type.Number()),
80
+ monthlyNewGrowthPct: Type.Optional(Type.Number()),
81
+ monthlyChurnPct: Type.Optional(Type.Number()),
82
+ revenuePerUnitPerMonth: Type.Optional(Type.Number()),
83
+ variableCostPerUnitPerMonth: Type.Optional(Type.Number()),
84
+ cacPerUnit: Type.Optional(Type.Number()),
85
+ });
86
+ const FixedCostSchema = Type.Object({
87
+ id: Type.String(),
88
+ name: Type.Optional(Type.String()),
89
+ monthlyAmount: Type.Optional(Type.Number()),
90
+ startMonth: Type.Optional(Type.Number()),
91
+ });
92
+ const OneTimeCostSchema = Type.Object({
93
+ id: Type.String(),
94
+ name: Type.Optional(Type.String()),
95
+ amount: Type.Optional(Type.Number()),
96
+ month: Type.Optional(Type.Number()),
97
+ });
98
+ pi.registerTool({
99
+ name: "tl_project",
100
+ label: "Thought Layer: project",
101
+ description:
102
+ "Run The Thought Layer's deterministic monthly business projection from structured assumptions (parties, fixed costs, one-time costs, horizon). Returns break-even, year-1 revenue and net, max cash drawdown, and ending MRR. Use this instead of estimating the numbers.",
103
+ parameters: Type.Object({
104
+ parties: Type.Array(PartySchema),
105
+ fixedCosts: Type.Optional(Type.Array(FixedCostSchema)),
106
+ oneTimeCosts: Type.Optional(Type.Array(OneTimeCostSchema)),
107
+ horizonMonths: Type.Optional(Type.Number()),
108
+ currency: Type.Optional(Type.String()),
109
+ }),
110
+ async execute(_id, params): Promise<ToolResult> {
111
+ const a = params as Assumptions;
112
+ const p = computeProjection(a);
113
+ if (!p) return text("No parties provided, so there is nothing to project.", {});
114
+ const s = p.summary;
115
+ const cur = a.currency || "USD";
116
+ const body = [
117
+ `Horizon: ${s.horizon} months`,
118
+ `Monthly break-even: ${s.breakEvenMonth ? `month ${s.breakEvenMonth}` : "beyond horizon"}`,
119
+ `Cumulative break-even: ${s.cumBreakEvenMonth ? `month ${s.cumBreakEvenMonth}` : "beyond horizon"}`,
120
+ `Year-1 revenue: ${fmtMoney(s.year1Revenue, cur)}`,
121
+ `Year-1 net: ${fmtMoney(s.year1Net, cur)}`,
122
+ `Max cash drawdown: ${fmtMoney(s.maxDrawdown, cur)}`,
123
+ `Ending MRR: ${fmtMoney(s.endingMRR, cur)}`,
124
+ ].join("\n");
125
+ return text(`Projection summary:\n${body}`, { summary: s });
126
+ },
127
+ });
128
+ }
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@hobocode/thought-layer",
3
+ "version": "0.1.0",
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
+ "license": "MIT",
6
+ "author": "Hobocode LLC <jerm@hobocode.net>",
7
+ "homepage": "https://weareallproductmanagersnow.com",
8
+ "type": "module",
9
+ "keywords": [
10
+ "pi-package",
11
+ "agent-skills",
12
+ "claude-code",
13
+ "product-validation",
14
+ "prd",
15
+ "domain-driven-design"
16
+ ],
17
+ "pi": {
18
+ "skills": ["./skills"],
19
+ "extensions": ["./extensions"],
20
+ "prompts": ["./prompts"]
21
+ },
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "exports": {
26
+ "./core": "./core/index.ts"
27
+ },
28
+ "scripts": {
29
+ "typecheck": "tsc --noEmit",
30
+ "test": "vitest run",
31
+ "test:watch": "vitest"
32
+ },
33
+ "dependencies": {
34
+ "@sinclair/typebox": "^0.34.0"
35
+ },
36
+ "peerDependencies": {
37
+ "@earendil-works/pi-coding-agent": ">=0.1.0"
38
+ },
39
+ "peerDependenciesMeta": {
40
+ "@earendil-works/pi-coding-agent": { "optional": true }
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^22.0.0",
44
+ "typescript": "^5.6.0",
45
+ "vitest": "^2.1.0"
46
+ },
47
+ "files": [
48
+ "skills",
49
+ "extensions",
50
+ "prompts",
51
+ "core/index.ts",
52
+ "core/scoring.ts",
53
+ "core/domains.ts",
54
+ "core/model.ts",
55
+ "README.md",
56
+ "LICENSE"
57
+ ]
58
+ }
@@ -0,0 +1,5 @@
1
+ Apply the **thought-layer-grill** skill. Grill the draft PRD one sharp question at a time: challenge it against the domain, sharpen the glossary, surface contradictions and missing requirements, and update the PRD inline until it is build-ready. Respect anything marked out of scope.
2
+
3
+ The Grill is the LAST design step and grills an existing PRD. If there is no PRD yet, say so first and recommend running `/tl` (or `/tl-prd` to draft one) rather than grilling a bare idea — only proceed cold if I tell you to.
4
+
5
+ Begin the grill on:
@@ -0,0 +1,3 @@
1
+ Apply the **thought-layer-naming** skill. Propose name candidates across a range of styles, grounded in this specific business and audience, each with a rationale and a domain-ready slug. Then use the `tl_domains` tool to check availability for the strongest slugs.
2
+
3
+ Name this:
@@ -0,0 +1,3 @@
1
+ Apply the **thought-layer-panel** skill to the answer below, at its stage's altitude. If no stage is given, treat it as the opening idea stage: judge whether the idea is clear, honest, real, and worth pursuing, and park any "how will it be built" concerns (implementation, UX, data, edge cases) for the Grill rather than raising them against the idea. Run all three personas, use the `tl_score` tool for the status and grade, and give the plain verdict plus at most three fixes that belong to this stage. Do not soften it, and do not move the goalposts into a later stage.
2
+
3
+ Evaluate this:
@@ -0,0 +1 @@
1
+ Apply the **thought-layer-prd** skill. Compose a complete first-draft PRD from everything validated so far (the idea and the business model) — including a first-cut domain glossary and testable requirements. This is the pre-grill draft: aim for build-ready, but flag the weakest assumptions and thinnest sections so the grill knows where to push. Keep the ubiquitous language exact. Carry any open to-dos forward. Output only the markdown document.
package/prompts/tl.md ADDED
@@ -0,0 +1,10 @@
1
+ Run the full Thought Layer framework on the idea below. Apply the **thought-layer-framework** skill as the backbone:
2
+
3
+ - Walk every stage in order: first validate the idea (what it is, domain knowledge, validation, market selection, the 30-second pitch), then make the business model real (time, costs, scale, pricing, the model and its numbers, acquisition, relationships, support).
4
+ - Evaluate each stage with the **thought-layer-panel** skill at that stage's altitude, and use the `tl_score` tool for the verdict. Use `tl_project` for the business-model numbers.
5
+ - Do not jump to the design phase. The Grill and the PRD come last, after the validation and model stages. Any "how will it actually be built" concern gets parked until the design phase (the PRD draft, then the grill), not raised against the early idea.
6
+ - When the framework reaches the design phase, run **thought-layer-prd** (draft the spec) then **thought-layer-grill** (grill the draft until it is build-ready). Use **thought-layer-naming** (with `tl_domains`) whenever it needs a name.
7
+
8
+ Take it one stage at a time, one stage per turn, and wait for the user between stages. If an idea is given below, treat it as the answer to stage 1 (the Concise What) and evaluate it before moving on. If nothing is below, open by asking the user for their idea in one sentence — do not wait to be handed a brief.
9
+
10
+ The idea (if any):