@hobocode/thought-layer 0.6.1 → 0.8.5
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 +7 -3
- package/core/artifacts-io.ts +146 -0
- package/core/artifacts.ts +690 -0
- package/core/index.ts +7 -0
- package/core/merge.ts +157 -0
- package/core/notion-io.ts +292 -0
- package/core/notion.ts +312 -0
- package/core/progress.ts +8 -5
- package/core/state-ops.ts +1 -1
- package/core/sync-io.ts +432 -0
- package/core/sync.ts +150 -0
- package/dist/tl.js +1866 -45
- package/extensions/thought-layer.ts +93 -2
- package/package.json +8 -1
- package/prompts/tl-artifacts.md +7 -0
- package/prompts/tl-compliance.md +7 -0
- package/prompts/tl-wiki.md +9 -0
- package/skills/thought-layer-compliance/SKILL.md +139 -0
- package/skills/thought-layer-framework/SKILL.md +9 -0
- package/skills/thought-layer-wiki/SKILL.md +43 -0
package/README.md
CHANGED
|
@@ -19,14 +19,16 @@ This is open source and BYOK by design. The point is to help people build real t
|
|
|
19
19
|
- **thought-layer-naming.** Name the thing, with rationale and domain-ready slugs.
|
|
20
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
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-wiki.** Auto-generate a private Notion wiki, an internal intranet that organizes everything the workflow built (the thinking, the brand and styles, the business model, the PRD, the deploy rules) and links every artifact you delivered to GitHub. Private to your own Notion workspace, BYOK token, no public exposure.
|
|
23
|
+
- **thought-layer-compliance.** Once the business is defined, research its governance, regulatory, compliance, licensing, and taxation-prep needs for your exact jurisdiction and entity type, and hand back a cited report (live links, costs, renewal cycles, filing deadlines, a critical path to launch) to review with your own legal and tax advisors. Research and a starting checklist, not legal or tax advice; it saves as the governance artifact, so it rides along into the GitHub bundle and the Notion wiki.
|
|
22
24
|
- **thought-layer-speedrun.** A fast, unranked path to a build-ready spec when you do not need the full panel and score.
|
|
23
25
|
- **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`.
|
|
24
26
|
|
|
25
27
|
**A Pi package** that adds, on top of the skills:
|
|
26
28
|
|
|
27
|
-
- **Deterministic tools** the agent can call so the math is exact and never re-derived: `tl_score` (confidence to status and grade), `tl_domains` (availability, BYOK), `tl_project` (the numeric business projection), `tl_state` (the portable progress file), `tl_scaffold` (a deterministic, deployable static site from the spec + brand),
|
|
28
|
-
- **Slash commands** (prompt templates): `/tl` runs the whole flow; `/tl-speedrun` is the fast unranked path; `/tl-panel`, `/tl-grill`, `/tl-prd`, `/tl-naming` run each stage; `/tl-build` builds the hardened PRD into a deploy-ready artifact; `/tl-deploy` takes it live.
|
|
29
|
-
- **A `tl` CLI** for any shell agent (`npx -y @hobocode/thought-layer tl ...`): `read`/`list`/`answer`/`feedback`/`artifact`/`cursor`/`export` for the shared progress file, `scaffold` for the deployable static-site floor,
|
|
29
|
+
- **Deterministic tools** the agent can call so the math is exact and never re-derived: `tl_score` (confidence to status and grade), `tl_domains` (availability, BYOK), `tl_project` (the numeric business projection), `tl_state` (the portable progress file), `tl_scaffold` (a deterministic, deployable static site from the spec + brand), `deploy` (take the build live to a URL you own), `tl_sync` (store and sync your sessions in your own private GitHub repo), `tl_artifacts` (deliver the full asset bundle to that repo), and `tl_wiki` (build a private Notion wiki from the session and those artifacts).
|
|
30
|
+
- **Slash commands** (prompt templates): `/tl` runs the whole flow; `/tl-speedrun` is the fast unranked path; `/tl-panel`, `/tl-grill`, `/tl-prd`, `/tl-naming` run each stage; `/tl-build` builds the hardened PRD into a deploy-ready artifact; `/tl-deploy` takes it live; `/tl-artifacts` delivers the full asset bundle to your repo; `/tl-wiki` builds a private Notion intranet from it; `/tl-compliance` researches your GRC, licensing, and tax readiness.
|
|
31
|
+
- **A `tl` CLI** for any shell agent (`npx -y @hobocode/thought-layer tl ...`): `read`/`list`/`answer`/`feedback`/`artifact`/`cursor`/`export` for the shared progress file, `scaffold` for the deployable static-site floor, `deploy` to take the build live, and `sync` to store and version your sessions in your own private GitHub repo.
|
|
30
32
|
|
|
31
33
|
## Install
|
|
32
34
|
|
|
@@ -71,6 +73,8 @@ The hosted version of the rigor lives at [weareallproductmanagersnow.com](https:
|
|
|
71
73
|
- **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
74
|
- **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
75
|
- **Backend deploy (done):** when `build.json` declares a backend, `/tl-deploy` (the `deploy` tool, or `tl deploy`) ships it automatically alongside the front end: the functions go up via your Netlify CLI and the declared env var names are set on the site (values read only from your environment, BYOK). `DATABASE_URL` is bring-your-own by default; `--provision-db` (your own Neon key) and `--apply-schema` (psql) are opt in, and `--static-only` ships just the front end. Owned, no lock-in.
|
|
76
|
+
- **Artifacts and wiki (done):** `tl artifacts` (the `tl_artifacts` tool) delivers the full asset bundle for a session, the PRD, brand guide + look book + logo, SWOT and business-model infographics, market research, a landing page, and any build/deploy provenance, to your own private sessions repo under `artifacts/<session>/`. `tl wiki` (the `tl_wiki` tool) then builds a **private Notion wiki**, an internal intranet that organizes all of it into a page per workflow area plus an Artifacts database that links each file. Notion pages are private to your own workspace (auth, not public); the integration token is BYOK from the environment. Free for one user, upgradeable for a team.
|
|
77
|
+
- **Sessions and collaboration (done):** `tl sync` (and the `tl_sync` tool) stores your session files in your OWN private GitHub repo. Save any number of named sessions (one private repo for your own projects, a separate repo per founder you collaborate with), list and open them, and sync. Git carries history and multi-user; the kit reconciles concurrent edits itself (newest wins per field, conflicts reported), so it never hand-merges JSON. Collaboration is granted on GitHub (you add collaborators, the kit never changes permissions). BYOK, no central account.
|
|
74
78
|
|
|
75
79
|
## Notes for contributors
|
|
76
80
|
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// Node IO for artifact delivery, shared by the `tl artifacts` CLI and the
|
|
2
|
+
// tl_artifacts Pi tool. The pure generation lives in artifacts.ts; this loads a
|
|
3
|
+
// session's state, builds the artifact bundle, copies any on-disk build/deploy
|
|
4
|
+
// provenance, writes it all under artifacts/<slug>/ in the user's own private
|
|
5
|
+
// sessions repo, and commits + pushes via the same git plumbing as sync-io.ts.
|
|
6
|
+
//
|
|
7
|
+
// Artifacts are generated and overwrite-wins: they are not field-merged like the
|
|
8
|
+
// session JSON, and pullAndReconcile already takes the remote copy of any
|
|
9
|
+
// non-session file, so this composes cleanly with sync.
|
|
10
|
+
//
|
|
11
|
+
// Secrets: nothing here reads a token. git uses its credential helper; no token
|
|
12
|
+
// is ever placed on argv or persisted.
|
|
13
|
+
|
|
14
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { basename, dirname, join } from "node:path";
|
|
16
|
+
import { git, isGitRepo, loadConfig, resolveWorkspace } from "./sync-io.ts";
|
|
17
|
+
import { slugify } from "./sync.ts";
|
|
18
|
+
import { STATE_DIR, loadStateFile } from "./state-file.ts";
|
|
19
|
+
import { buildArtifactSet, type ArtifactFile, type ArtifactKind } from "./artifacts.ts";
|
|
20
|
+
import type { StateOpResult } from "./state-ops.ts";
|
|
21
|
+
|
|
22
|
+
export interface ArtifactsRunOptions {
|
|
23
|
+
path?: string; // explicit source state file (else the session in the clone)
|
|
24
|
+
name?: string; // session name (the artifacts subfolder slug)
|
|
25
|
+
workspace?: string; // select an existing sessions workspace by label
|
|
26
|
+
dir?: string; // explicit clone dir
|
|
27
|
+
message?: string; // commit message
|
|
28
|
+
noPush?: boolean; // commit only, do not push
|
|
29
|
+
noDeliver?: boolean; // write the bundle into the clone but do not commit/push
|
|
30
|
+
domain?: string; // landing-page domain
|
|
31
|
+
founderName?: string; // landing-page founder
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const ok = (message: string, details: Record<string, unknown> = {}): StateOpResult => ({ ok: true, message, details });
|
|
35
|
+
const fail = (message: string, details: Record<string, unknown> = {}): StateOpResult => ({ ok: false, message, details });
|
|
36
|
+
|
|
37
|
+
// On-disk build/deploy provenance, looked for relative to the source state file.
|
|
38
|
+
// build.json/deploy.json and the .md provenance sit in the .thought-layer/ dir;
|
|
39
|
+
// schema.sql/netlify.toml/BACKEND.md sit at the project root beside it.
|
|
40
|
+
const STATE_DIR_ARTIFACTS = ["build.json", "deploy.json", "TRACEABILITY.md", "DECISIONS.md"];
|
|
41
|
+
const ROOT_ARTIFACTS = ["BACKEND.md", "schema.sql", "netlify.toml", ".env.example"];
|
|
42
|
+
|
|
43
|
+
const kindOf = (path: string): ArtifactKind =>
|
|
44
|
+
path.endsWith(".md") ? "markdown" : path.endsWith(".svg") ? "svg" : path.endsWith(".html") ? "html" : path.endsWith(".json") ? "json" : "text";
|
|
45
|
+
|
|
46
|
+
// Parse "owner/name", "https://github.com/owner/name(.git)", or "git@github.com:owner/name"
|
|
47
|
+
// into "owner/name". Returns null when it is a local path or unrecognizable.
|
|
48
|
+
export function repoOwnerName(repo: string): string | null {
|
|
49
|
+
const m = String(repo || "").trim().match(/(?:github\.com[:/])?([\w.-]+)\/([\w.-]+?)(?:\.git)?$/);
|
|
50
|
+
if (!m) return null;
|
|
51
|
+
if (m[1] === "." || m[1] === "..") return null;
|
|
52
|
+
return `${m[1]}/${m[2]}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function runArtifacts(opts: ArtifactsRunOptions, ctx: { generatedAt: string }): StateOpResult {
|
|
56
|
+
try {
|
|
57
|
+
const cfg = loadConfig();
|
|
58
|
+
const { cloneDir, ws } = resolveWorkspace(opts as never, cfg);
|
|
59
|
+
if (!isGitRepo(cloneDir)) {
|
|
60
|
+
return fail(`No sessions workspace at ${cloneDir}. Run tl sync init --repo <owner/name> first, then save a session.`, { cloneDir });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const slug = slugify(opts.name || ws?.activeSession?.replace(/\.json$/, "") || "");
|
|
64
|
+
if (!slug) return fail("Name the session whose artifacts to deliver: tl artifacts --name <name>.");
|
|
65
|
+
|
|
66
|
+
// Source state precedence mirrors sync save: an explicit --path or
|
|
67
|
+
// THOUGHT_LAYER_STATE wins; otherwise the session file in the clone.
|
|
68
|
+
const sessionPath = join(cloneDir, STATE_DIR, `${slug}.json`);
|
|
69
|
+
const useExplicit = !!((opts.path && opts.path.trim()) || (process.env["THOUGHT_LAYER_STATE"] || "").trim());
|
|
70
|
+
const loadTarget = useExplicit ? opts.path : existsSync(sessionPath) ? sessionPath : opts.path;
|
|
71
|
+
const loaded = loadStateFile(loadTarget);
|
|
72
|
+
if (!loaded.exists) {
|
|
73
|
+
return fail(`No session "${slug}" found (looked at ${loaded.path}). Save it first with tl sync save --name ${slug}.`, { cloneDir });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const { files, manifest } = buildArtifactSet(loaded.state, {
|
|
77
|
+
generatedAt: ctx.generatedAt,
|
|
78
|
+
domain: opts.domain,
|
|
79
|
+
founderName: opts.founderName,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Copy any on-disk build/deploy provenance beside the source state file into
|
|
83
|
+
// a Deploy/ subfolder, extending the manifest with each one.
|
|
84
|
+
const srcStateDir = dirname(loaded.path);
|
|
85
|
+
const srcRoot = basename(srcStateDir) === STATE_DIR ? dirname(srcStateDir) : srcStateDir;
|
|
86
|
+
const extra: ArtifactFile[] = [];
|
|
87
|
+
const copyIfPresent = (fromDir: string, fname: string): void => {
|
|
88
|
+
const from = join(fromDir, fname);
|
|
89
|
+
if (!existsSync(from)) return;
|
|
90
|
+
try {
|
|
91
|
+
const content = readFileSync(from, "utf8");
|
|
92
|
+
const rel = `Deploy/${fname}`;
|
|
93
|
+
files[rel] = content;
|
|
94
|
+
extra.push({ path: rel, bytes: Buffer.byteLength(content, "utf8"), kind: kindOf(rel), source: "build/deploy" });
|
|
95
|
+
} catch { /* skip unreadable provenance */ }
|
|
96
|
+
};
|
|
97
|
+
STATE_DIR_ARTIFACTS.forEach((f) => copyIfPresent(srcStateDir, f));
|
|
98
|
+
ROOT_ARTIFACTS.forEach((f) => copyIfPresent(srcRoot, f));
|
|
99
|
+
manifest.files = [...manifest.files, ...extra].sort((a, b) => a.path.localeCompare(b.path));
|
|
100
|
+
|
|
101
|
+
// Write the bundle under artifacts/<slug>/ in the clone, plus the manifest.
|
|
102
|
+
const baseRel = join("artifacts", slug);
|
|
103
|
+
const baseAbs = join(cloneDir, baseRel);
|
|
104
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
105
|
+
const dest = join(baseAbs, rel);
|
|
106
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
107
|
+
writeFileSync(dest, content);
|
|
108
|
+
}
|
|
109
|
+
writeFileSync(join(baseAbs, "artifacts.json"), JSON.stringify(manifest, null, 2) + "\n");
|
|
110
|
+
|
|
111
|
+
const fileCount = Object.keys(files).length + 1; // + artifacts.json
|
|
112
|
+
if (opts.noDeliver) {
|
|
113
|
+
return ok(`Generated ${fileCount} artifact file(s) into ${baseAbs} (local only; not committed).`,
|
|
114
|
+
{ cloneDir, dir: baseAbs, session: slug, files: Object.keys(files), count: fileCount, committed: false, pushed: false });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Force-add past the sessions .gitignore (which ignores dist/, build.json,
|
|
118
|
+
// etc.): the delivered artifacts under artifacts/ are intentionally tracked.
|
|
119
|
+
git(cloneDir, ["add", "-f", "--", baseRel]);
|
|
120
|
+
const msg = opts.message || `Deliver artifacts for ${slug}`;
|
|
121
|
+
const committed = git(cloneDir, ["commit", "-m", msg]).status === 0;
|
|
122
|
+
let pushed = false;
|
|
123
|
+
let pushNote = "";
|
|
124
|
+
if (committed && !opts.noPush) {
|
|
125
|
+
const p = git(cloneDir, ["push"]);
|
|
126
|
+
pushed = p.status === 0;
|
|
127
|
+
if (!pushed) pushNote = ` Could not push (${(p.err || "").split("\n")[0] || "see git output"}); commit is local, run tl sync push when ready.`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Derive clickable GitHub URLs for the wiki to link to.
|
|
131
|
+
const branch = git(cloneDir, ["rev-parse", "--abbrev-ref", "HEAD"]).out.trim() || ws?.defaultBranch || "main";
|
|
132
|
+
const ownerName = repoOwnerName(ws?.repo || "");
|
|
133
|
+
const githubBase = ownerName ? `https://github.com/${ownerName}/blob/${branch}/${baseRel}` : null;
|
|
134
|
+
const urls: Record<string, string> = {};
|
|
135
|
+
if (githubBase) for (const rel of Object.keys(files)) urls[rel] = `${githubBase}/${rel.split("/").map(encodeURIComponent).join("/")}`;
|
|
136
|
+
|
|
137
|
+
return ok(
|
|
138
|
+
`Delivered ${fileCount} artifact file(s) for "${slug}" to ${baseRel} in ${cloneDir}.` +
|
|
139
|
+
`${committed ? (opts.noPush ? " Committed locally (no push)." : pushed ? " Committed and pushed." : pushNote) : " Nothing changed since the last delivery."}` +
|
|
140
|
+
`${githubBase ? `\nView on GitHub: ${githubBase}` : ""}`,
|
|
141
|
+
{ cloneDir, dir: baseRel, session: slug, files: Object.keys(files), count: fileCount, committed, pushed, branch, repo: ownerName, githubBase, urls },
|
|
142
|
+
);
|
|
143
|
+
} catch (e) {
|
|
144
|
+
return fail(`tl_artifacts error: ${(e as Error).message}`);
|
|
145
|
+
}
|
|
146
|
+
}
|