create-substrat 0.4.2 → 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/index.js CHANGED
@@ -162,6 +162,12 @@ dist/
162
162
  *.db
163
163
  .data/
164
164
  .DS_Store
165
+
166
+ # Which kernel version this checkout last announced to an agent (.substrat/hooks
167
+ # /session-start.mjs). Per-checkout state, not a project fact.
168
+ .substrat/.docs-pin
169
+ # Opt out of that hook entirely by creating this file.
170
+ .substrat/no-session-context
165
171
  `;
166
172
 
167
173
  function readme(name) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-substrat",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "description": "Scaffold a Substrat vertical — `npm create substrat <dir>`.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -0,0 +1,15 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/claude-code-settings.json",
3
+ "hooks": {
4
+ "SessionStart": [
5
+ {
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "node \"${CLAUDE_PROJECT_DIR}/.substrat/hooks/session-start.mjs\""
10
+ }
11
+ ]
12
+ }
13
+ ]
14
+ }
15
+ }
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * SessionStart hook — the project announces itself (#754).
4
+ *
5
+ * The smooth part of a framework's agent integration is not the skills, it is
6
+ * that the user never has to remember the framework *has* an integration. This
7
+ * runs when an agent session starts, and if the project is a Substrat vertical
8
+ * it hands the agent three things it would otherwise have to discover: what this
9
+ * project is, where the rules live, and **which version of the docs describes the
10
+ * kernel actually installed here**.
11
+ *
12
+ * That last one is the reason this exists. Substrat is 0.x and interfaces change
13
+ * without notice, so an agent working from pages it cached two minors ago is the
14
+ * expensive failure — confident, plausible, and wrong. `llms.txt` is published at
15
+ * a version-pinned URL precisely so this hook can point at the matching slice.
16
+ *
17
+ * ## Deliberately not tool-specific
18
+ *
19
+ * This file lives in `.substrat/` — the tool-neutral home, next to `playbook.md` —
20
+ * not in `.claude/`. `.claude/settings.json` is a three-line adapter that runs it,
21
+ * and any other client that grows a session hook binds the same way. It also
22
+ * means the plugin distribution (#753) can ship this script unchanged rather than
23
+ * forking it.
24
+ *
25
+ * ## Deliberately silent, and deliberately offline
26
+ *
27
+ * It prints nothing at all unless `package.json` has a `substrat` block, so it is
28
+ * inert in any other project. And it makes **no network request**: session start
29
+ * is on the critical path of every session and is frequently offline, while the
30
+ * check a fetch would perform — does the published doc set still describe my
31
+ * kernel — is already mechanical for whoever actually fetches, because
32
+ * `/llms-<version>.txt` 404s exactly when the answer is no.
33
+ *
34
+ * Opt out by creating `.substrat/no-session-context`.
35
+ */
36
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
37
+ import { dirname, join } from 'node:path';
38
+
39
+ const DOCS = 'https://substrat.net';
40
+ const KERNEL = '@substrat-run/kernel';
41
+
42
+ /** Where the project is. Claude Code sets this; fall back to the cwd it ran us in. */
43
+ const root = process.env.CLAUDE_PROJECT_DIR || process.cwd();
44
+
45
+ /** The marker: which kernel version this project last announced. */
46
+ const MARKER = join(root, '.substrat', '.docs-pin');
47
+ const OPT_OUT = join(root, '.substrat', 'no-session-context');
48
+
49
+ /** Nothing this hook does is worth failing a session over. */
50
+ function silent() {
51
+ process.exit(0);
52
+ }
53
+
54
+ function readJson(path) {
55
+ try {
56
+ return JSON.parse(readFileSync(path, 'utf8'));
57
+ } catch {
58
+ return undefined;
59
+ }
60
+ }
61
+
62
+ function main() {
63
+ if (existsSync(OPT_OUT)) silent();
64
+
65
+ const pkg = readJson(join(root, 'package.json'));
66
+ // The detection signal is the block `substrat push` already reads — no sentinel
67
+ // file to invent, and nothing to keep in sync.
68
+ if (!pkg?.substrat) silent();
69
+
70
+ if (existsSync(OPT_OUT)) silent();
71
+
72
+ // What is actually installed beats what package.json asked for: a caret range on
73
+ // 0.x pins the minor, and the resolved version is what the code compiles against.
74
+ const installed = readJson(join(root, 'node_modules', KERNEL, 'package.json'))?.version;
75
+ const declared = pkg.dependencies?.[KERNEL] ?? pkg.devDependencies?.[KERNEL];
76
+
77
+ const lines = [
78
+ 'This project is a **Substrat vertical** — a multi-tenant business app on the',
79
+ 'Substrat kernel and its engines.',
80
+ '',
81
+ '- The always-on rules you must not violate are in `AGENTS.md` — module-code',
82
+ ' boundaries, the gates, and the two checkpoints you may never self-approve.',
83
+ '- The build flow (interview → design → reshape → checkpoints) is `.substrat/playbook.md`.',
84
+ ' It is a playbook, not always-on context: read it when starting or extending a vertical.',
85
+ ];
86
+
87
+ if (installed) {
88
+ lines.push(
89
+ '',
90
+ `This project has **${KERNEL} ${installed}** installed.`,
91
+ '',
92
+ `Docs describing that exact version: ${DOCS}/llms-${installed}.txt`,
93
+ '',
94
+ 'That URL returns 200 only while the published docs still describe this kernel. A 404',
95
+ `means they have moved on — fetch ${DOCS}/llms.txt instead, and treat anything you`,
96
+ 'already believe about the API as unverified. Substrat is pre-1.0 and interfaces change',
97
+ 'without notice, so do not answer from memory about its surface; the pages are markdown',
98
+ 'at those URLs and every doc page has a `.md` twin.',
99
+ );
100
+
101
+ const announced = existsSync(MARKER) ? readFileSync(MARKER, 'utf8').trim() : '';
102
+ if (announced && announced !== installed) {
103
+ lines.unshift(
104
+ `**The kernel moved: ${announced} → ${installed} since the last session here.**`,
105
+ 'Re-read the docs slice below before relying on anything you remember about the API.',
106
+ '',
107
+ );
108
+ }
109
+ try {
110
+ mkdirSync(dirname(MARKER), { recursive: true });
111
+ writeFileSync(MARKER, `${installed}\n`);
112
+ } catch {
113
+ // A read-only checkout still gets the context; it just re-announces next time.
114
+ }
115
+ } else {
116
+ lines.push(
117
+ '',
118
+ `**${KERNEL} is not installed yet** (\`package.json\` asks for \`${declared ?? 'it'}\`).`,
119
+ 'Run the install before trusting any version-specific guidance, then re-check the docs',
120
+ `index at ${DOCS}/llms.txt.`,
121
+ );
122
+ }
123
+
124
+ process.stdout.write(
125
+ `${JSON.stringify({
126
+ hookSpecificOutput: {
127
+ hookEventName: 'SessionStart',
128
+ additionalContext: lines.join('\n'),
129
+ },
130
+ })}\n`,
131
+ );
132
+ }
133
+
134
+ try {
135
+ main();
136
+ } catch {
137
+ // Never break a session because orientation failed.
138
+ silent();
139
+ }