solid2-agent-kit 0.4.5 → 0.8.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
@@ -16,7 +16,12 @@ Supported agents: **Cursor** and **Claude Code**.
16
16
  | Always-applied hard rules | `.cursor/rules/solid-2.mdc` (glob-attached to `*.tsx`/`*.jsx`) | managed block in `CLAUDE.md` | 23 hard rules + banned Solid 1.x API table + eslint-plugin-solid lint-heritage table. Injected without the agent having to decide to read anything |
17
17
  | Agent skill | `.cursor/skills/solid-2/` | `.claude/skills/solid-2/` | Execution-model primer, decision tables, canonical patterns, a “still runs / write the other form” table, review checklist, official-doc URL index |
18
18
  | Shared agent context | managed block in `AGENTS.md` | same (Claude Code users can reference it from `CLAUDE.md`) | Core principles and pointers, always in context |
19
- | Mechanical gate | `solid2-kit check` | same | Whole-file regex guard over `src/**/*.{ts,tsx}`: fails on props destructuring (including multi-line signatures), `{ ...props }` rest copies, `{list().map(...)}` in JSX, React imports/hooks/JSX props/`React.lazy`/`useOptimistic`/`onDoubleClick`, Solid 1.x imports/APIs/components/JSX namespaces, `vite-plugin-solid`, old-router `<Route>`/`<A>`/`<HashRouter>`/`<FileRoutes>`, `MetaProvider`, `Context.Provider`, camelCase style keys, `key` props, `value()!` hand-narrowing, hand-rolled loading signals, `=== undefined` readiness branches, SolidStart leftovers (`@solidjs/start`, `h3`, `createAsync`, `useSubmission`, `"use client"`), Next.js imports/`useRouter`/`typeof window`, `history.pushState`, `render(<App />)` (pass a function), `isPending(user())` / `latest(user())` (pass the accessor), and `<Dynamic>` (use `dynamic()`) |
19
+ | Mechanical gate | `solid2-kit check` | same | Comment-stripped regex guard over `src/**/*.{ts,tsx,jsx}` (or explicit `[files...]`): fails on props destructuring (including multi-line signatures), `{ ...props }` rest copies, `{list().map(...)}` in JSX, React imports/hooks/JSX props/`React.lazy`/`useOptimistic`/`onDoubleClick`, Solid 1.x imports/APIs/components/JSX namespaces, `vite-plugin-solid`, old-router `<Route>`/`<A>`/`<HashRouter>`/`<FileRoutes>`, `MetaProvider`, `Context.Provider`, camelCase style keys, `key` props, `value()!` hand-narrowing, hand-rolled loading signals, `=== undefined` readiness branches, SolidStart leftovers (`@solidjs/start`, `h3`, `createAsync`, `useSubmission`, `"use client"`), Next.js imports/`useRouter`/`typeof window`, `history.pushState`, `render(<App />)` (pass a function), `isPending(user())` / `latest(user())` (pass the accessor), `<Dynamic>` (use `dynamic()`), `import type { JSX } from "solid-js"` (no `JSX` namespace there — `Element` from `solid-js`, DOM `JSX` types from `@solidjs/web`), and two-phase effects whose apply only writes a local signal (use a writable derivation) |
20
+ | Edit-time hook | `postToolUse` entry in `.cursor/hooks.json` | `PostToolUse` entry in `.claude/settings.json` (matcher includes `Bash`) | Runs the mechanical gate automatically on **every agent file edit — including edits made through shell commands** (sed, heredocs, codemods; source paths mentioned in the command are checked after it ran) — and feeds findings straight back into the conversation (Cursor: `additional_context`; Claude Code: stderr + exit 2), so enforcement does not depend on the agent remembering to run `check`. Wired only when the kit is a local devDependency; opt out with `--no-hooks` |
21
+ | Turn-end gate | — | `Stop` entry in `.claude/settings.json` | Whole-project `check` + `doctor` when the agent tries to end its turn: with findings in place the stop is blocked (exit 2) and the findings are fed back, so a turn cannot finish with React/Solid 1.x patterns left behind. Loop-safe via `stop_hook_active` (one forced continuation, never an infinite loop) |
22
+ | Pre-execution guard | `preToolUse` + `beforeShellExecution` entries in `.cursor/hooks.json` | `PreToolUse` entry in `.claude/settings.json` | Denies actions **before they run**: edits/deletes of kit-owned guardrail files (an agent blocked by a gate will try to remove the gate), edits that would strip the kit's hook entries or managed blocks, uninspectable shell rewrites of those files, and `npm/pnpm/yarn/bun install` of banned dependencies (`react`, `vite-plugin-solid`, `eslint-plugin-solid`, SolidStart, ...) — caught proactively instead of post-hoc by `doctor` |
23
+ | Project-wiring gate | `solid2-kit doctor` | same | Config drift the source gate cannot see: React / `vite-plugin-solid` / `eslint-plugin-solid` / SolidStart deps in `package.json`, a 1.x `solid-js` range, `jsx` ≠ `preserve` or `jsxImportSource` ≠ `@solidjs/web` in tsconfig, Solid 1.x wiring in root config files, and stale installed guidance (kit updated but `sync` not re-run) |
24
+ | Review command | `.cursor/commands/solid2-review.md` | `.claude/commands/solid2-review.md` | `/solid2-review` — on-demand deep review: runs both gates, then walks the diff against the skill checklist targeting the semantic failure classes regexes cannot see (post-`await` reads, effect misuse, unkeyed server rows, boundary placement, context snapshots) |
20
25
 
21
26
  Managed blocks are delimited with `<!-- solid2-agent-kit:*:start/end -->` markers; everything
22
27
  outside them is yours. Kit-owned files (`solid-2.mdc`, the skill directories) are overwritten
@@ -35,11 +40,16 @@ npx github:lightsound/solid2-agent-kit init
35
40
  npx github:lightsound/solid2-agent-kit init --cursor
36
41
  npx github:lightsound/solid2-agent-kit init --claude
37
42
 
38
- # run the mechanical pattern gate (default source dir: src)
43
+ # run the mechanical pattern gate (default source dir: src; or name files)
39
44
  npx github:lightsound/solid2-agent-kit check
40
45
  npx github:lightsound/solid2-agent-kit check --dir app
46
+ npx github:lightsound/solid2-agent-kit check src/App.tsx
41
47
 
42
- # pull in kit updates later (idempotent; replaces managed blocks and kit-owned files)
48
+ # check the project wiring (deps, tsconfig, root configs)
49
+ npx github:lightsound/solid2-agent-kit doctor
50
+
51
+ # pull in kit updates later (idempotent; replaces managed blocks, kit-owned
52
+ # files, and hook entries)
43
53
  npx github:lightsound/solid2-agent-kit sync
44
54
  ```
45
55
 
@@ -58,6 +68,71 @@ Update later with `bun update solid2-agent-kit` (or the npm/pnpm equivalent) fol
58
68
  `npx solid2-agent-kit sync`. Outside a project that has the kit installed, prefer the
59
69
  `npx github:...` form — it always tracks this repo directly.
60
70
 
71
+ ## Edit-time hooks (automatic enforcement)
72
+
73
+ The rules and skill teach; `check` verifies — but only if someone runs it. The hook layer
74
+ closes that gap: `init`/`sync` wire `solid2-kit hook` into both agents' hook systems, so the
75
+ mechanical gate runs on **every file the agent edits** and violations come back as feedback
76
+ the agent must act on, in the same turn it introduced them:
77
+
78
+ - **Cursor** — a `postToolUse` entry in `.cursor/hooks.json`; findings are injected into the
79
+ conversation as `additional_context` right after the tool result.
80
+ - **Claude Code** — a `PostToolUse` entry in `.claude/settings.json` (matcher
81
+ `Edit|MultiEdit|Write|Bash`); findings go to stderr with exit 2, which Claude Code feeds
82
+ back to the model as a correction prompt.
83
+
84
+ Agents also edit files *through the shell* (sed, heredocs, `mv`, codemod scripts), which
85
+ never hits the Edit/Write tools — so shell-shaped tool payloads are scanned for source
86
+ paths mentioned in the command, and those files are checked after the command ran.
87
+
88
+ Claude Code additionally gets a **turn-end gate**: a `Stop` hook re-runs the whole
89
+ mechanical check (over `src`/`app`/`lib`) plus `doctor` when the agent tries to finish its
90
+ turn, and blocks the stop while findings remain. `stop_hook_active` is honored, so an agent
91
+ that cannot satisfy the gate is forced to continue exactly once, never looped forever.
92
+ (Cursor's `stop` hook cannot feed back to the agent, so there the last line of defense is
93
+ the always-attached rules plus CI.)
94
+
95
+ Both agents also get a **pre-execution guard** (Claude: `PreToolUse`; Cursor: `preToolUse`
96
+ and `beforeShellExecution`). A blocked agent's classic next move is to decide the gate
97
+ itself is wrong — edit the rules file, delete the skill, or strip the hook entries. The
98
+ guard denies, before the tool call runs: writes to kit-owned files (rules `.mdc`, skill
99
+ directories, review commands, the kit under `node_modules`); edits to `.cursor/hooks.json`
100
+ / `.claude/settings.json` / `AGENTS.md` / `CLAUDE.md` that would remove the kit's entries
101
+ or managed blocks (marker-preserving edits pass — agents can still add the user's own
102
+ hooks and sections); shell rewrites of those files (`sed`/`rm`/redirects — uninspectable,
103
+ so the deny message redirects to the Edit tool); and package-manager installs of
104
+ dependencies `doctor` bans (exact token match, so `react` is denied but `react-aria` is
105
+ not). The deny message always tells the agent what to do instead — fix the flagged code,
106
+ run `solid2-kit sync`, or stop and ask the user. Pre-execution events never run the
107
+ content check itself: fixing a currently-bad file is always allowed.
108
+
109
+ When the kit is installed as a devDependency, `init` also wires a `lint:solid` script
110
+ (`solid2-kit check && solid2-kit doctor`) into the project's `package.json` (only if the
111
+ script does not already exist), so agents and CI can run both gates by name.
112
+
113
+ The hook command is `node node_modules/solid2-agent-kit/bin/solid2-kit.mjs hook <agent>`,
114
+ so it only works when the kit is installed as a devDependency (see above); `init` skips the
115
+ wiring and prints a note otherwise. Merging is idempotent — kit entries are refreshed in
116
+ place on `sync`, user entries are preserved. Opt out with `init --no-hooks`. Hooks never
117
+ break the agent loop: clean edits, read tools, and malformed payloads exit silently, and
118
+ reports are capped at 40 findings.
119
+
120
+ Note on gate precision: `check` (and therefore the hooks) matches against
121
+ **comment-stripped** source, so prose like `// migrated off createResource` never trips
122
+ the gate, while string checks (`"use client"`) still work.
123
+
124
+ ## CI for consuming projects
125
+
126
+ Run both gates on every PR so nothing an agent (or human) merges can reintroduce React or
127
+ Solid 1.x patterns:
128
+
129
+ ```yaml
130
+ - uses: actions/setup-node@v4
131
+ with: { node-version: 22 }
132
+ - run: npm ci
133
+ - run: npx solid2-agent-kit check && npx solid2-agent-kit doctor
134
+ ```
135
+
61
136
  ## Why not eslint-plugin-solid?
62
137
 
63
138
  `eslint-plugin-solid` was built for Solid 1.x. Its analyzer predates the Solid 2 model and
@@ -88,12 +163,13 @@ markdown with an `.md` suffix).
88
163
  ## Layout
89
164
 
90
165
  ```
91
- bin/solid2-kit.mjs CLI: init / sync / check
166
+ bin/solid2-kit.mjs CLI: init / sync / check / doctor / hook
92
167
  files/shared/rules-body.md hard rules (rendered into .mdc and CLAUDE.md)
93
168
  files/shared/agents-section.md AGENTS.md managed-block content
94
169
  files/skills/solid-2/ SKILL.md + references/official-docs.md
170
+ files/commands/solid2-review.md /solid2-review command (Cursor + Claude Code)
95
171
  scripts/check-docs-drift.mjs weekly docs cross-check
96
- tests/ mechanical-gate fixtures (`npm test`)
172
+ tests/ check/doctor fixtures + hook and init-merge tests (`npm test`)
97
173
  ```
98
174
 
99
175
  ## Publishing (npm)
@@ -2,12 +2,15 @@
2
2
  // solid2-agent-kit CLI — installs and maintains Solid 2.0 guidance for AI
3
3
  // coding agents (Cursor and Claude Code), and runs a mechanical pattern gate.
4
4
  //
5
- // solid2-kit init [--cursor] [--claude] [--target <dir>]
6
- // solid2-kit sync [--cursor] [--claude] [--target <dir>] (alias of init)
7
- // solid2-kit check [--dir <srcdir>] [--target <dir>]
5
+ // solid2-kit init [--cursor] [--claude] [--no-hooks] [--target <dir>]
6
+ // solid2-kit sync [--cursor] [--claude] [--no-hooks] [--target <dir>] (alias of init)
7
+ // solid2-kit check [--dir <srcdir>] [--target <dir>] [files...]
8
+ // solid2-kit doctor [--target <dir>]
9
+ // solid2-kit hook (claude|cursor) (stdin: agent hook JSON payload)
8
10
  //
9
- // `init`/`sync` are idempotent: kit-owned files are overwritten and managed
10
- // blocks in AGENTS.md / CLAUDE.md are replaced in place.
11
+ // `init`/`sync` are idempotent: kit-owned files are overwritten, managed
12
+ // blocks in AGENTS.md / CLAUDE.md are replaced in place, and hook entries in
13
+ // .cursor/hooks.json / .claude/settings.json are merged without duplicates.
11
14
 
12
15
  import {
13
16
  cpSync,
@@ -32,6 +35,20 @@ function flagValue(name, fallback) {
32
35
  return index === -1 || index + 1 >= args.length ? fallback : args[index + 1];
33
36
  }
34
37
 
38
+ const VALUE_FLAGS = new Set(['--dir', '--target']);
39
+
40
+ function positionalArgs() {
41
+ const out = [];
42
+ for (let i = 1; i < args.length; i += 1) {
43
+ if (args[i].startsWith('--')) {
44
+ if (VALUE_FLAGS.has(args[i])) i += 1;
45
+ continue;
46
+ }
47
+ out.push(args[i]);
48
+ }
49
+ return out;
50
+ }
51
+
35
52
  // --- init / sync ------------------------------------------------------------
36
53
 
37
54
  function countOccurrences(haystack, needle) {
@@ -74,6 +91,104 @@ function writeKitOwnedFile(filePath, body) {
74
91
  return filePath;
75
92
  }
76
93
 
94
+ // Hook commands run from the project root without node_modules/.bin on PATH,
95
+ // so they invoke the locally installed kit through node directly. `init` only
96
+ // wires hooks when that file exists (kit installed as a dependency).
97
+ const KIT_LOCAL_BIN = 'node_modules/solid2-agent-kit/bin/solid2-kit.mjs';
98
+
99
+ function readJsonConfig(filePath) {
100
+ if (!existsSync(filePath)) return {};
101
+ try {
102
+ return JSON.parse(readFileSync(filePath, 'utf8'));
103
+ } catch {
104
+ console.warn(
105
+ `solid2-kit — warning: ${filePath} is not valid JSON. File left untouched; fix it and re-run sync to wire the edit hook.`,
106
+ );
107
+ return null;
108
+ }
109
+ }
110
+
111
+ function writeJsonConfig(filePath, config) {
112
+ mkdirSync(dirname(filePath), { recursive: true });
113
+ writeFileSync(filePath, `${JSON.stringify(config, null, 2)}\n`);
114
+ return filePath;
115
+ }
116
+
117
+ // Replace the existing kit-owned entry (identified by the marker in its
118
+ // command) or append a fresh one — sync then updates matchers/commands that
119
+ // changed between kit versions while leaving user entries untouched.
120
+ function upsertHookEntry(entries, marker, entry, matches) {
121
+ const index = entries.findIndex((existing) => matches(existing, marker));
122
+ if (index === -1) entries.push(entry);
123
+ else entries[index] = entry;
124
+ }
125
+
126
+ const cursorEntryMatches = (entry, marker) =>
127
+ typeof entry?.command === 'string' && entry.command.includes(marker);
128
+ const claudeEntryMatches = (entry, marker) =>
129
+ (entry?.hooks ?? []).some((item) => typeof item?.command === 'string' && item.command.includes(marker));
130
+
131
+ // Cursor hooks: postToolUse (post-edit check injected as additional_context)
132
+ // plus preToolUse / beforeShellExecution (pre-execution guard: guardrail
133
+ // tampering and banned-dependency installs are denied before they run).
134
+ function upsertCursorHook(target) {
135
+ const filePath = join(target, '.cursor/hooks.json');
136
+ const config = readJsonConfig(filePath);
137
+ if (config === null) return null;
138
+ config.version ??= 1;
139
+ config.hooks ??= {};
140
+ const command = `node ${KIT_LOCAL_BIN} hook cursor`;
141
+ for (const event of ['postToolUse', 'preToolUse', 'beforeShellExecution']) {
142
+ upsertHookEntry((config.hooks[event] ??= []), 'solid2-kit.mjs hook cursor', { command }, cursorEntryMatches);
143
+ }
144
+ return writeJsonConfig(filePath, config);
145
+ }
146
+
147
+ // Claude Code hooks: PostToolUse (`hook claude` prints findings to stderr and
148
+ // exits 2, which Claude Code feeds back into the model as correction context;
149
+ // Bash is matched so shell edits via sed/heredocs are gated too), PreToolUse
150
+ // (pre-execution guard, may deny) and Stop (whole-project final gate — the
151
+ // agent cannot end its turn with findings).
152
+ function upsertClaudeHook(target) {
153
+ const filePath = join(target, '.claude/settings.json');
154
+ const config = readJsonConfig(filePath);
155
+ if (config === null) return null;
156
+ config.hooks ??= {};
157
+ const command = `node ${KIT_LOCAL_BIN} hook claude`;
158
+ const toolMatcher = 'Edit|MultiEdit|Write|Bash';
159
+ upsertHookEntry(
160
+ (config.hooks.PreToolUse ??= []),
161
+ 'solid2-kit.mjs hook claude',
162
+ { matcher: toolMatcher, hooks: [{ type: 'command', command, timeout: 30 }] },
163
+ claudeEntryMatches,
164
+ );
165
+ upsertHookEntry(
166
+ (config.hooks.PostToolUse ??= []),
167
+ 'solid2-kit.mjs hook claude',
168
+ { matcher: toolMatcher, hooks: [{ type: 'command', command, timeout: 30 }] },
169
+ claudeEntryMatches,
170
+ );
171
+ upsertHookEntry(
172
+ (config.hooks.Stop ??= []),
173
+ 'solid2-kit.mjs hook claude',
174
+ { hooks: [{ type: 'command', command, timeout: 60 }] },
175
+ claudeEntryMatches,
176
+ );
177
+ return writeJsonConfig(filePath, config);
178
+ }
179
+
180
+ // Wire a `lint:solid` script (referenced by the AGENTS.md guidance) into the
181
+ // consuming project so agents and CI can run both gates by name.
182
+ function upsertLintScript(target) {
183
+ const filePath = join(target, 'package.json');
184
+ if (!existsSync(filePath)) return null;
185
+ const pkg = readJsonConfig(filePath);
186
+ if (pkg === null) return null;
187
+ if (pkg.scripts?.['lint:solid']) return null;
188
+ (pkg.scripts ??= {})['lint:solid'] = 'solid2-kit check && solid2-kit doctor';
189
+ return writeJsonConfig(filePath, pkg);
190
+ }
191
+
77
192
  function copySkill(targetSkillDir) {
78
193
  // The skill directory is kit-owned: remove it first so files deleted in
79
194
  // newer kit versions do not linger after a sync.
@@ -92,6 +207,7 @@ function init() {
92
207
 
93
208
  const rulesBody = readFileSync(join(KIT_ROOT, 'files/shared/rules-body.md'), 'utf8');
94
209
  const agentsSection = readFileSync(join(KIT_ROOT, 'files/shared/agents-section.md'), 'utf8');
210
+ const reviewCommand = readFileSync(join(KIT_ROOT, 'files/commands/solid2-review.md'), 'utf8');
95
211
  const written = [];
96
212
 
97
213
  if (wantCursor) {
@@ -109,10 +225,12 @@ function init() {
109
225
  ].join('\n');
110
226
  written.push(writeKitOwnedFile(join(target, '.cursor/rules/solid-2.mdc'), mdc));
111
227
  written.push(copySkill(join(target, '.cursor/skills/solid-2')));
228
+ written.push(writeKitOwnedFile(join(target, '.cursor/commands/solid2-review.md'), reviewCommand));
112
229
  }
113
230
 
114
231
  if (wantClaude) {
115
232
  written.push(copySkill(join(target, '.claude/skills/solid-2')));
233
+ written.push(writeKitOwnedFile(join(target, '.claude/commands/solid2-review.md'), reviewCommand));
116
234
  // Claude Code has no glob-attached rules mechanism; CLAUDE.md is always
117
235
  // loaded, so the full rules body lives there in a managed block.
118
236
  written.push(
@@ -134,12 +252,108 @@ function init() {
134
252
  ),
135
253
  );
136
254
 
255
+ // Edit-time hooks: run the mechanical gate automatically on every agent
256
+ // file edit, so enforcement does not depend on the agent remembering to
257
+ // run `solid2-kit check`.
258
+ let hooksNote = null;
259
+ if (!args.includes('--no-hooks')) {
260
+ if (existsSync(join(target, KIT_LOCAL_BIN))) {
261
+ if (wantCursor) written.push(upsertCursorHook(target));
262
+ if (wantClaude) written.push(upsertClaudeHook(target));
263
+ written.push(upsertLintScript(target));
264
+ } else {
265
+ hooksNote =
266
+ 'edit-time hooks not wired: install the kit as a devDependency ("solid2-agent-kit": "github:lightsound/solid2-agent-kit") so `node node_modules/solid2-agent-kit/bin/solid2-kit.mjs` resolves, then re-run sync. Pass --no-hooks to silence this note.';
267
+ }
268
+ }
269
+
137
270
  console.log(`solid2-agent-kit v${VERSION} — installed for ${[wantCursor && 'Cursor', wantClaude && 'Claude Code'].filter(Boolean).join(' + ')}:`);
138
271
  for (const path of written.filter(Boolean)) console.log(` ${relative(target, path) || '.'}`);
272
+ if (hooksNote) console.log(` note: ${hooksNote}`);
139
273
  }
140
274
 
141
275
  // --- check ------------------------------------------------------------------
142
276
 
277
+ // Balanced-scan helpers for the effect-sync check: regexes cannot pair the
278
+ // parentheses of a createEffect(...) call, so scan (string-aware; comments
279
+ // are already stripped) for the matching closer from an opening bracket.
280
+ function scanBalanced(content, openIndex) {
281
+ let depth = 0;
282
+ let quote = null;
283
+ for (let i = openIndex; i < content.length; i += 1) {
284
+ const ch = content[i];
285
+ if (quote !== null) {
286
+ if (ch === '\\') i += 1;
287
+ else if (ch === quote) quote = null;
288
+ } else if (ch === "'" || ch === '"' || ch === '`') {
289
+ quote = ch;
290
+ } else if (ch === '(' || ch === '{' || ch === '[') {
291
+ depth += 1;
292
+ } else if (ch === ')' || ch === '}' || ch === ']') {
293
+ depth -= 1;
294
+ if (depth === 0) return i;
295
+ }
296
+ }
297
+ return -1;
298
+ }
299
+
300
+ function splitTopLevelArgs(argsText) {
301
+ const parts = [];
302
+ let depth = 0;
303
+ let quote = null;
304
+ let start = 0;
305
+ for (let i = 0; i < argsText.length; i += 1) {
306
+ const ch = argsText[i];
307
+ if (quote !== null) {
308
+ if (ch === '\\') i += 1;
309
+ else if (ch === quote) quote = null;
310
+ } else if (ch === "'" || ch === '"' || ch === '`') {
311
+ quote = ch;
312
+ } else if (ch === '(' || ch === '{' || ch === '[') {
313
+ depth += 1;
314
+ } else if (ch === ')' || ch === '}' || ch === ']') {
315
+ depth -= 1;
316
+ } else if (ch === ',' && depth === 0) {
317
+ parts.push(argsText.slice(start, i));
318
+ start = i + 1;
319
+ }
320
+ }
321
+ parts.push(argsText.slice(start));
322
+ return parts;
323
+ }
324
+
325
+ // An apply phase that is nothing but one (optionally if-guarded) call to a
326
+ // local signal setter is "a signal plus an effect that syncs it" — rule 4.
327
+ // Restricting to setters declared via createSignal destructuring in the same
328
+ // file keeps external set* functions (setDefaultOptions, setTimeout) out.
329
+ function soleLocalSetterApply(apply, localSetters) {
330
+ const head = apply.match(
331
+ /^(?:async\s*)?(?:\([^()]*\)|[\w$]+)\s*=>\s*\{?\s*(?:if\s*\((?:[^()]|\([^()]*\))*\)\s*\{?\s*)?(set[A-Z][\w$]*)\s*\(/,
332
+ );
333
+ if (!head || !localSetters.has(head[1])) return false;
334
+ const close = scanBalanced(apply, head[0].length - 1);
335
+ if (close === -1) return false;
336
+ return /^[\s;}]*$/.test(apply.slice(close + 1));
337
+ }
338
+
339
+ const LOCAL_SETTER_DECL = /\[\s*[\w$]+\s*,\s*(set[A-Z][\w$]*)\s*\]\s*=\s*createSignal\b/g;
340
+
341
+ function effectSyncFindings(content) {
342
+ const localSetters = new Set([...content.matchAll(LOCAL_SETTER_DECL)].map((m) => m[1]));
343
+ if (localSetters.size === 0) return [];
344
+ const matches = [];
345
+ for (const call of content.matchAll(/\bcreateEffect\s*(?:<[^>()]*>)?\s*\(/g)) {
346
+ const open = call.index + call[0].length - 1;
347
+ const close = scanBalanced(content, open);
348
+ if (close === -1) continue;
349
+ const args = splitTopLevelArgs(content.slice(open + 1, close));
350
+ if (args.length >= 2 && soleLocalSetterApply(args[1].trim(), localSetters)) {
351
+ matches.push({ index: call.index });
352
+ }
353
+ }
354
+ return matches;
355
+ }
356
+
143
357
  const CHECKS = [
144
358
  {
145
359
  id: 'props-destructure-param',
@@ -164,6 +378,20 @@ const CHECKS = [
164
378
  message:
165
379
  'Solid 1.x import path. Stores/merge/omit come from "solid-js"; render/hydrate/Portal/Dynamic come from "@solidjs/web".',
166
380
  },
381
+ {
382
+ id: 'jsx-namespace-import',
383
+ pattern: /import\s+(?:type\s+)?\{[^}]*\bJSX\b[^}]*\}\s*from\s+['"]solid-js['"]/g,
384
+ message:
385
+ 'Solid 2 does not export a `JSX` namespace from "solid-js" (TS2305). Children/return types are `Element` from "solid-js"; DOM-specific JSX types (JSX.IntrinsicElements, JSX.CSSProperties) come from "@solidjs/web".',
386
+ },
387
+ {
388
+ // A two-phase createEffect whose apply is nothing but one (optionally
389
+ // if-guarded) call to a setter declared via createSignal in this file.
390
+ id: 'effect-sync-signal',
391
+ find: effectSyncFindings,
392
+ message:
393
+ 'Effect that only syncs a local signal — "state + effect" (rule 4). Derive the value instead; to reset state when another reactive value changes, use a writable derivation: createSignal(() => { source(); return initial; }).',
394
+ },
167
395
  {
168
396
  id: 'react-jsx-prop',
169
397
  pattern: /\b(?:className|htmlFor)=/g,
@@ -367,8 +595,59 @@ const CHECKS = [
367
595
  },
368
596
  ];
369
597
 
370
- // .ts / .tsx sources, excluding .d.ts declaration files.
371
- const SOURCE_FILE = /(?<!\.d)\.tsx?$/;
598
+ // .ts / .tsx / .jsx sources, excluding .d.ts declaration files.
599
+ const SOURCE_FILE = /(?<!\.d)\.(?:tsx?|jsx)$/;
600
+
601
+ // Blank comment interiors (preserving newlines and offsets) so banned tokens
602
+ // mentioned in comments ("migrated off createResource") do not trip the gate.
603
+ // String and template contents are preserved — some checks match string
604
+ // literals ("use client"). This is a heuristic scanner, not a JS lexer: a
605
+ // regex literal containing escaped slashes or `//` in JSX text can blank the
606
+ // remainder of that one line (false negatives only, never false positives).
607
+ function stripComments(content) {
608
+ let out = '';
609
+ let state = 'code';
610
+ for (let i = 0; i < content.length; i += 1) {
611
+ const ch = content[i];
612
+ const next = content[i + 1];
613
+ if (state === 'code') {
614
+ if (ch === '/' && (next === '/' || next === '*')) {
615
+ state = next === '/' ? 'line' : 'block';
616
+ out += ' ';
617
+ i += 1;
618
+ } else {
619
+ if (ch === "'") state = 'single';
620
+ else if (ch === '"') state = 'double';
621
+ else if (ch === '`') state = 'template';
622
+ out += ch;
623
+ }
624
+ } else if (state === 'line') {
625
+ if (ch === '\n') state = 'code';
626
+ out += ch === '\n' ? ch : ' ';
627
+ } else if (state === 'block') {
628
+ if (ch === '*' && next === '/') {
629
+ state = 'code';
630
+ out += ' ';
631
+ i += 1;
632
+ } else {
633
+ out += ch === '\n' ? ch : ' ';
634
+ }
635
+ } else {
636
+ out += ch;
637
+ if (ch === '\\') {
638
+ out += next ?? '';
639
+ i += 1;
640
+ } else if (
641
+ (state === 'single' && ch === "'") ||
642
+ (state === 'double' && ch === '"') ||
643
+ (state === 'template' && ch === '`')
644
+ ) {
645
+ state = 'code';
646
+ }
647
+ }
648
+ }
649
+ return out;
650
+ }
372
651
 
373
652
  function* walk(dir) {
374
653
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
@@ -378,36 +657,435 @@ function* walk(dir) {
378
657
  }
379
658
  }
380
659
 
660
+ // One formatted finding per pattern match: "path:line [id] message\n > source".
661
+ // Patterns run against comment-stripped content (same offsets); the quoted
662
+ // source line comes from the original file.
663
+ function fileFindings(file, relativeTo) {
664
+ const raw = readFileSync(file, 'utf8');
665
+ const content = stripComments(raw);
666
+ const lines = raw.split('\n');
667
+ const findings = [];
668
+ for (const rule of CHECKS) {
669
+ for (const match of rule.find ? rule.find(content) : content.matchAll(rule.pattern)) {
670
+ const lineNumber = content.slice(0, match.index).split('\n').length;
671
+ findings.push(
672
+ `${relative(relativeTo, file) || file}:${lineNumber} [${rule.id}] ${rule.message}\n > ${lines[lineNumber - 1].trim()}`,
673
+ );
674
+ }
675
+ }
676
+ return findings;
677
+ }
678
+
381
679
  function check() {
382
680
  const target = resolve(flagValue('--target', '.'));
383
- const srcDir = resolve(target, flagValue('--dir', 'src'));
384
- if (!existsSync(srcDir)) {
385
- console.error(`solid2-kit check — source directory not found: ${srcDir} (use --dir)`);
386
- process.exit(2);
681
+ const fileArgs = positionalArgs();
682
+
683
+ let files;
684
+ if (fileArgs.length > 0) {
685
+ // Explicit file mode (used by agent hooks): check only the named sources.
686
+ files = fileArgs.map((file) => resolve(target, file)).filter((file) => SOURCE_FILE.test(file));
687
+ } else {
688
+ const srcDir = resolve(target, flagValue('--dir', 'src'));
689
+ if (!existsSync(srcDir)) {
690
+ console.error(`solid2-kit check — source directory not found: ${srcDir} (use --dir)`);
691
+ process.exit(2);
692
+ }
693
+ files = [...walk(srcDir)];
387
694
  }
388
695
 
389
696
  let findings = 0;
390
- let files = 0;
391
- for (const file of walk(srcDir)) {
392
- files += 1;
393
- const content = readFileSync(file, 'utf8');
394
- const lines = content.split('\n');
395
- for (const rule of CHECKS) {
396
- for (const match of content.matchAll(rule.pattern)) {
397
- findings += 1;
398
- const lineNumber = content.slice(0, match.index).split('\n').length;
399
- console.error(
400
- `${relative(target, file)}:${lineNumber} [${rule.id}] ${rule.message}\n > ${lines[lineNumber - 1].trim()}`,
697
+ for (const file of files) {
698
+ for (const finding of fileFindings(file, target)) {
699
+ findings += 1;
700
+ console.error(finding);
701
+ }
702
+ }
703
+
704
+ if (findings > 0) {
705
+ console.error(`\nsolid2-kit check — ${findings} finding(s) in ${files.length} file(s).`);
706
+ process.exit(1);
707
+ }
708
+ console.log(`solid2-kit check — OK (${files.length} files scanned).`);
709
+ }
710
+
711
+ // --- hook -------------------------------------------------------------------
712
+ // Edit-time gate wired by `init` into the agents' hook systems. Reads the
713
+ // hook JSON payload from stdin, runs the mechanical checks on the edited
714
+ // source files, and feeds findings back through each agent's channel:
715
+ // claude — Claude Code PostToolUse: findings on stderr + exit 2 (Claude
716
+ // sees the stderr and fixes; other exit codes are ignored). Also
717
+ // handles the Stop event as a whole-project final gate: exit 2
718
+ // blocks the agent from ending its turn with violations in place.
719
+ // cursor — Cursor postToolUse: findings as {"additional_context"} JSON on
720
+ // stdout (injected into the conversation after the tool result).
721
+ // Both agents also edit files through shell commands (sed, heredocs, mv,
722
+ // codemods), which never hit the Edit/Write tools — so shell-shaped tool
723
+ // payloads are scanned for source paths mentioned in the command and those
724
+ // files are checked after the command ran.
725
+ // Hooks must never break the agent loop: malformed payloads exit 0 silently.
726
+
727
+ const WRITE_TOOL = /write|edit|replace|apply|patch/i;
728
+ const SHELL_TOOL = /shell|bash|terminal|command|exec/i;
729
+ const MAX_REPORTED = 40;
730
+
731
+ function formatReport(header, findings) {
732
+ const shown = findings.slice(0, MAX_REPORTED);
733
+ const more = findings.length - shown.length;
734
+ return `${header}\n\n${shown.join('\n')}${more > 0 ? `\n…and ${more} more finding(s).` : ''}`;
735
+ }
736
+
737
+ function isCheckableSource(path) {
738
+ return SOURCE_FILE.test(path) && !path.includes('\n') && !path.includes('node_modules');
739
+ }
740
+
741
+ function collectSourcePaths(value, found = new Set()) {
742
+ if (typeof value === 'string') {
743
+ if (isCheckableSource(value) && existsSync(value)) found.add(value);
744
+ } else if (Array.isArray(value)) {
745
+ for (const item of value) collectSourcePaths(item, found);
746
+ } else if (value && typeof value === 'object') {
747
+ for (const item of Object.values(value)) collectSourcePaths(item, found);
748
+ }
749
+ return found;
750
+ }
751
+
752
+ // Path-shaped tokens ending in a source extension, resolved against the
753
+ // command's cwd. Existence filtering keeps false positives at zero.
754
+ function collectCommandPaths(command, cwd, found = new Set()) {
755
+ if (typeof command !== 'string') return found;
756
+ for (const match of command.matchAll(/[^\s'"`;|&()<>=]+\.(?:tsx?|jsx)\b/g)) {
757
+ const token = match[0];
758
+ if (!isCheckableSource(token)) continue;
759
+ const path = resolve(cwd, token);
760
+ if (existsSync(path)) found.add(path);
761
+ }
762
+ return found;
763
+ }
764
+
765
+ // --- pre-execution guard (self-defense + proactive denial) -------------------
766
+ // Agents that keep getting blocked by a gate tend to conclude the gate is
767
+ // wrong and edit or delete it. Pre-execution hooks (Claude PreToolUse,
768
+ // Cursor preToolUse / beforeShellExecution) can deny a tool call before it
769
+ // runs, so this guard refuses:
770
+ // - edits/deletes of kit-owned guardrail files (rules, skill, commands,
771
+ // the kit itself under node_modules),
772
+ // - edits that would remove the kit's markers from hook configs or the
773
+ // AGENTS.md / CLAUDE.md managed blocks,
774
+ // - shell rewrites of those files (sed/rm/redirects — uninspectable),
775
+ // - installing dependencies doctor would flag anyway (react,
776
+ // vite-plugin-solid, eslint-plugin-solid, SolidStart, ...).
777
+ // Everything else is allowed; this gate never inspects file *content* being
778
+ // written (that is the post-edit hook's job).
779
+
780
+ const GUARDRAIL_PATH =
781
+ /\.cursor\/rules\/solid-2\.mdc|(?:\.cursor|\.claude)\/skills\/solid-2|(?:\.cursor|\.claude)\/commands\/solid2-review\.md|node_modules\/solid2-agent-kit\//;
782
+ const MARKER_FILE = /(?:\.claude\/settings\.json|\.cursor\/hooks\.json|AGENTS\.md|CLAUDE\.md)$/;
783
+ const KIT_MARKER = 'solid2-agent-kit';
784
+ const SHELL_WRITE = /\b(?:rm|mv|cp|sed|tee|truncate|unlink|chmod|perl|python[\d.]*)\b|>>?/;
785
+ const INSTALL_CMD = /\b(?:npm|pnpm|yarn|bun)\s+(?:-\S+\s+)*(?:install|add|i)\b/;
786
+
787
+ const TAMPER_MESSAGE =
788
+ 'solid2-agent-kit guardrail: this file is kit-managed Solid 2.0 guidance/enforcement. Do not edit, weaken, or delete it — fix the flagged code instead. Kit files update via `solid2-kit sync`. If you believe the guardrail itself is wrong, or the user explicitly wants it removed, stop and tell the user.';
789
+
790
+ function bannedInstall(command) {
791
+ if (!INSTALL_CMD.test(command)) return null;
792
+ for (const raw of command.split(/\s+/)) {
793
+ const token = raw.replace(/^['"]|['"]$/g, '');
794
+ const name = token.replace(/(?!^)@.*$/, '');
795
+ if (name in BANNED_DEPS) return name;
796
+ }
797
+ return null;
798
+ }
799
+
800
+ function preToolGate(agent, payload) {
801
+ const deny = (agentMessage) => {
802
+ if (agent === 'claude') {
803
+ console.error(agentMessage);
804
+ process.exit(2);
805
+ }
806
+ console.log(
807
+ JSON.stringify({
808
+ permission: 'deny',
809
+ agent_message: agentMessage,
810
+ user_message: 'solid2-agent-kit guardrail blocked this action.',
811
+ }),
812
+ );
813
+ process.exit(0);
814
+ };
815
+
816
+ let toolInput = payload.tool_input;
817
+ if (typeof toolInput === 'string') {
818
+ try {
819
+ toolInput = JSON.parse(toolInput);
820
+ } catch {
821
+ toolInput = undefined;
822
+ }
823
+ }
824
+ const toolName = typeof payload.tool_name === 'string' ? payload.tool_name : '';
825
+
826
+ // Shell commands: beforeShellExecution carries `command` at the top level,
827
+ // PreToolUse(Bash) inside tool_input.
828
+ const command =
829
+ typeof payload.command === 'string'
830
+ ? payload.command
831
+ : SHELL_TOOL.test(toolName) && typeof toolInput?.command === 'string'
832
+ ? toolInput.command
833
+ : null;
834
+ if (command !== null) {
835
+ const banned = bannedInstall(command);
836
+ if (banned) {
837
+ deny(
838
+ `solid2-agent-kit guardrail: do not install "${banned}". ${BANNED_DEPS[banned]} (\`solid2-kit doctor\` fails on it.)`,
839
+ );
840
+ }
841
+ if (SHELL_WRITE.test(command)) {
842
+ if (GUARDRAIL_PATH.test(command)) deny(TAMPER_MESSAGE);
843
+ if (/(?:\.claude\/settings\.json|\.cursor\/hooks\.json)/.test(command)) {
844
+ deny(
845
+ 'solid2-agent-kit guardrail: rewriting hook configs through the shell cannot be verified. Use the Edit tool (keeping the solid2-agent-kit hook entries), or ask the user.',
401
846
  );
402
847
  }
403
848
  }
849
+ process.exit(0);
404
850
  }
405
851
 
406
- if (findings > 0) {
407
- console.error(`\nsolid2-kit check — ${findings} finding(s) in ${files} file(s).`);
852
+ if (!WRITE_TOOL.test(toolName)) process.exit(0);
853
+ const filePath = typeof toolInput?.file_path === 'string' ? toolInput.file_path : toolInput?.path;
854
+ if (typeof filePath !== 'string') process.exit(0);
855
+
856
+ if (GUARDRAIL_PATH.test(filePath)) deny(TAMPER_MESSAGE);
857
+
858
+ if (MARKER_FILE.test(filePath)) {
859
+ // Editing these files is fine (users add their own hooks/sections through
860
+ // agents) as long as the kit's entries/managed blocks survive the edit.
861
+ const removalMessage = `solid2-agent-kit guardrail: this edit removes the ${KIT_MARKER} entries/managed block from ${filePath}. Keep them intact; run \`solid2-kit sync\` to update them. If the user explicitly wants the kit removed, stop and tell the user to do it.`;
862
+ if (typeof toolInput.content === 'string') {
863
+ let existing = '';
864
+ try {
865
+ existing = readFileSync(resolve(typeof payload.cwd === 'string' ? payload.cwd : process.cwd(), filePath), 'utf8');
866
+ } catch {
867
+ // New file — nothing to preserve.
868
+ }
869
+ if (existing.includes(KIT_MARKER) && !toolInput.content.includes(KIT_MARKER)) deny(removalMessage);
870
+ } else if (
871
+ typeof toolInput.old_string === 'string' &&
872
+ toolInput.old_string.includes(KIT_MARKER) &&
873
+ !String(toolInput.new_string ?? '').includes(KIT_MARKER)
874
+ ) {
875
+ deny(removalMessage);
876
+ }
877
+ }
878
+
879
+ process.exit(0);
880
+ }
881
+
882
+ // Claude Code Stop hook: the per-edit hooks catch violations file by file,
883
+ // but the agent can still end a turn with violations introduced through
884
+ // uncovered paths (git operations, scripts writing files, moved code). The
885
+ // Stop gate re-runs the whole mechanical check plus the wiring doctor and
886
+ // refuses to let the turn end while findings remain. `stop_hook_active`
887
+ // marks a turn that is already continuing because of this gate — allow it
888
+ // through then, so an agent that cannot satisfy the gate never loops forever.
889
+ function stopGate(payload) {
890
+ if (payload.stop_hook_active) process.exit(0);
891
+ const target = process.cwd();
892
+
893
+ const findings = [];
894
+ for (const dir of ['src', 'app', 'lib']) {
895
+ const abs = join(target, dir);
896
+ if (!existsSync(abs)) continue;
897
+ for (const file of walk(abs)) findings.push(...fileFindings(file, target));
898
+ }
899
+ if (existsSync(join(target, 'package.json'))) {
900
+ findings.push(...doctorFindings(target).map(({ id, message }) => `[${id}] ${message}`));
901
+ }
902
+ if (findings.length === 0) process.exit(0);
903
+
904
+ console.error(
905
+ formatReport(
906
+ 'solid2-kit — React/Solid 1.x patterns are still in the project; fix them before finishing:',
907
+ findings,
908
+ ),
909
+ );
910
+ process.exit(2);
911
+ }
912
+
913
+ function hook(agent) {
914
+ if (agent !== 'claude' && agent !== 'cursor') {
915
+ console.error('solid2-kit hook — expected agent: claude | cursor');
916
+ process.exit(2);
917
+ }
918
+
919
+ let payload;
920
+ try {
921
+ payload = JSON.parse(readFileSync(0, 'utf8'));
922
+ } catch {
923
+ process.exit(0);
924
+ }
925
+ if (!payload || typeof payload !== 'object') process.exit(0);
926
+
927
+ const event = typeof payload.hook_event_name === 'string' ? payload.hook_event_name : '';
928
+ if (agent === 'claude' && event === 'Stop') stopGate(payload);
929
+ // Pre-execution events (PreToolUse / preToolUse / beforeShellExecution)
930
+ // only run the guard — never the content check (the file has not been
931
+ // written yet, so checking would block edits based on pre-edit state).
932
+ if (/^(?:pre|before)/i.test(event)) preToolGate(agent, payload);
933
+
934
+ // afterFileEdit-style payloads carry file_path at the top level; tool-use
935
+ // payloads carry the edited path(s) inside tool_input. Only write-shaped
936
+ // and shell-shaped tools are gated, so reads/searches never are.
937
+ const files = new Set();
938
+ if (typeof payload.file_path === 'string') collectSourcePaths(payload.file_path, files);
939
+ if (payload.tool_input !== undefined) {
940
+ const toolName = typeof payload.tool_name === 'string' ? payload.tool_name : '';
941
+ let toolInput = payload.tool_input;
942
+ if (typeof toolInput === 'string') {
943
+ try {
944
+ toolInput = JSON.parse(toolInput);
945
+ } catch {
946
+ toolInput = undefined;
947
+ }
948
+ }
949
+ if (toolName === '' || WRITE_TOOL.test(toolName)) {
950
+ collectSourcePaths(toolInput, files);
951
+ } else if (SHELL_TOOL.test(toolName)) {
952
+ const cwd = typeof payload.cwd === 'string' ? payload.cwd : process.cwd();
953
+ collectCommandPaths(toolInput?.command, cwd, files);
954
+ }
955
+ }
956
+ if (files.size === 0) process.exit(0);
957
+
958
+ const findings = [];
959
+ for (const file of files) {
960
+ try {
961
+ findings.push(...fileFindings(file, process.cwd()));
962
+ } catch {
963
+ // File disappeared between the edit and the hook — nothing to check.
964
+ }
965
+ }
966
+ if (findings.length === 0) process.exit(0);
967
+
968
+ const report = formatReport(
969
+ 'solid2-kit check — React/Solid 1.x patterns in this edit; fix them now:',
970
+ findings,
971
+ );
972
+ if (agent === 'claude') {
973
+ console.error(report);
974
+ process.exit(2);
975
+ }
976
+ console.log(JSON.stringify({ additional_context: report }));
977
+ process.exit(0);
978
+ }
979
+
980
+ // --- doctor -----------------------------------------------------------------
981
+ // Project-config gate: `check` covers TSX sources, `doctor` covers the wiring
982
+ // around them — dependencies, tsconfig, and root config files — where agents
983
+ // also reach for React / Solid 1.x tooling (vite-plugin-solid,
984
+ // eslint-plugin-solid, jsxImportSource: "solid-js", ...).
985
+
986
+ const BANNED_DEPS = {
987
+ react: 'React does not belong in a Solid 2.0 project.',
988
+ 'react-dom': 'React does not belong in a Solid 2.0 project.',
989
+ next: 'Next.js does not belong in a Solid 2.0 project.',
990
+ 'vite-plugin-solid': 'Solid 1.x Vite plugin. Use @solidjs/vite-plugin.',
991
+ 'eslint-plugin-solid':
992
+ 'Built for Solid 1.x; its analyzer misreads Solid 2 idioms (two-phase createEffect, writable derivations, draft setters). Remove it — solid2-kit check carries the still-valid intents.',
993
+ '@solidjs/start': 'SolidStart 1.x. Solid 2 start mode lives in @solidjs/vite-plugin (start: true).',
994
+ 'solid-start': 'SolidStart 0.x. Solid 2 start mode lives in @solidjs/vite-plugin (start: true).',
995
+ 'solid-app-router': 'Pre-1.0 router. Use @solidjs/router 2 (createRouter({ routes })).',
996
+ vinxi: 'SolidStart 1.x toolchain. Solid 2 uses @solidjs/vite-plugin directly.',
997
+ };
998
+
999
+ const CONFIG_SOURCE = /\.(?:m|c)?[jt]s$/;
1000
+
1001
+ // Files installed by this kit that carry a version marker. When the kit
1002
+ // dependency is updated but `sync` is not re-run, the installed guidance
1003
+ // keeps teaching agents the older content — silent drift doctor can catch.
1004
+ const GUIDANCE_FILES = ['.cursor/rules/solid-2.mdc', 'CLAUDE.md', 'AGENTS.md'];
1005
+
1006
+ function doctorFindings(target) {
1007
+ const findings = [];
1008
+ const report = (id, message) => findings.push({ id, message });
1009
+
1010
+ const pkg = JSON.parse(readFileSync(join(target, 'package.json'), 'utf8'));
1011
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
1012
+ for (const [name, message] of Object.entries(BANNED_DEPS)) {
1013
+ if (name in deps) report(`dep-${name.replace(/[@/]/g, '')}`, `package.json depends on "${name}". ${message}`);
1014
+ }
1015
+ const solidRange = deps['solid-js'];
1016
+ if (typeof solidRange === 'string' && /^[\s^~=v]*[01]\./.test(solidRange)) {
1017
+ report('solid-js-version', `package.json pins solid-js "${solidRange}" — this kit teaches Solid 2.x; upgrade to ^2.`);
1018
+ }
1019
+
1020
+ for (const entry of readdirSync(target, { withFileTypes: true })) {
1021
+ if (!entry.isFile()) continue;
1022
+ const path = join(target, entry.name);
1023
+
1024
+ if (/^tsconfig.*\.json$/.test(entry.name)) {
1025
+ // tsconfig is JSONC; extract the two relevant keys with regexes.
1026
+ const content = readFileSync(path, 'utf8');
1027
+ const jsx = content.match(/"jsx"\s*:\s*"([^"]+)"/)?.[1];
1028
+ if (jsx && jsx !== 'preserve') {
1029
+ report('tsconfig-jsx', `${entry.name} sets "jsx": "${jsx}". Solid 2 compiles its own JSX: use "preserve".`);
1030
+ }
1031
+ const importSource = content.match(/"jsxImportSource"\s*:\s*"([^"]+)"/)?.[1];
1032
+ if (importSource && importSource !== '@solidjs/web') {
1033
+ report(
1034
+ 'tsconfig-jsx-import-source',
1035
+ `${entry.name} sets "jsxImportSource": "${importSource}". Solid 2 JSX types live in "@solidjs/web".`,
1036
+ );
1037
+ }
1038
+ continue;
1039
+ }
1040
+
1041
+ if (CONFIG_SOURCE.test(entry.name)) {
1042
+ const content = readFileSync(path, 'utf8');
1043
+ if (/['"]vite-plugin-solid['"]/.test(content)) {
1044
+ report('config-vite-plugin-solid', `${entry.name} references vite-plugin-solid (Solid 1.x). Import solid from "@solidjs/vite-plugin".`);
1045
+ }
1046
+ if (/jsxImportSource['"]?\s*[:=]\s*['"](?:solid-js|react)['"]/.test(content)) {
1047
+ report('config-jsx-import-source', `${entry.name} sets jsxImportSource to solid-js/react. Solid 2 uses "@solidjs/web".`);
1048
+ }
1049
+ }
1050
+
1051
+ if (/^(?:\.eslintrc|eslint\.config\.)/.test(entry.name)) {
1052
+ const content = readFileSync(path, 'utf8');
1053
+ if (content.includes('eslint-plugin-solid')) {
1054
+ report('eslint-plugin-solid', `${entry.name} wires eslint-plugin-solid (Solid 1.x analyzer; false-positives on Solid 2 idioms). Remove it.`);
1055
+ }
1056
+ }
1057
+ }
1058
+
1059
+ for (const guidance of GUIDANCE_FILES) {
1060
+ const path = join(target, guidance);
1061
+ if (!existsSync(path)) continue;
1062
+ const installed = readFileSync(path, 'utf8').match(/solid2-agent-kit v(\d[\w.-]*)/)?.[1];
1063
+ if (installed && installed !== VERSION) {
1064
+ report(
1065
+ 'stale-guidance',
1066
+ `${guidance} was installed by solid2-agent-kit v${installed} but v${VERSION} is running — run \`solid2-kit sync\` so agents read current guidance.`,
1067
+ );
1068
+ }
1069
+ }
1070
+
1071
+ return findings;
1072
+ }
1073
+
1074
+ function doctor() {
1075
+ const target = resolve(flagValue('--target', '.'));
1076
+ if (!existsSync(join(target, 'package.json'))) {
1077
+ console.error(`solid2-kit doctor — no package.json at ${target} (use --target)`);
1078
+ process.exit(2);
1079
+ }
1080
+
1081
+ const findings = doctorFindings(target);
1082
+ for (const { id, message } of findings) console.error(`[${id}] ${message}`);
1083
+
1084
+ if (findings.length > 0) {
1085
+ console.error(`\nsolid2-kit doctor — ${findings.length} finding(s). Fix the project wiring above.`);
408
1086
  process.exit(1);
409
1087
  }
410
- console.log(`solid2-kit check — OK (${files} files scanned).`);
1088
+ console.log('solid2-kit doctor — OK (dependencies, tsconfig, root configs, and installed guidance look like Solid 2).');
411
1089
  }
412
1090
 
413
1091
  // --- entry ------------------------------------------------------------------
@@ -420,15 +1098,23 @@ switch (command) {
420
1098
  case 'check':
421
1099
  check();
422
1100
  break;
1101
+ case 'doctor':
1102
+ doctor();
1103
+ break;
1104
+ case 'hook':
1105
+ hook(args[1]);
1106
+ break;
423
1107
  default:
424
1108
  console.log(
425
1109
  [
426
1110
  `solid2-agent-kit v${VERSION}`,
427
1111
  '',
428
1112
  'Usage:',
429
- ' solid2-kit init [--cursor] [--claude] [--target <dir>] install/update guidance (default: both tools)',
430
- ' solid2-kit sync [--cursor] [--claude] [--target <dir>] alias of init (idempotent)',
431
- ' solid2-kit check [--dir <srcdir>] [--target <dir>] mechanical React/Solid 1.x pattern gate (default dir: src)',
1113
+ ' solid2-kit init [--cursor] [--claude] [--no-hooks] [--target <dir>] install/update guidance + edit hooks (default: both tools)',
1114
+ ' solid2-kit sync [--cursor] [--claude] [--no-hooks] [--target <dir>] alias of init (idempotent)',
1115
+ ' solid2-kit check [--dir <srcdir>] [--target <dir>] [files...] mechanical React/Solid 1.x pattern gate (default dir: src)',
1116
+ ' solid2-kit doctor [--target <dir>] project-wiring gate: deps, tsconfig, root configs',
1117
+ ' solid2-kit hook (claude|cursor) edit-time gate for agent hooks (stdin: hook JSON payload)',
432
1118
  ].join('\n'),
433
1119
  );
434
1120
  process.exit(command ? 2 : 0);
@@ -0,0 +1,30 @@
1
+ Review the current change set for Solid 2.0 correctness. This project bans React patterns
2
+ and Solid 1.x APIs; the mechanical gates catch tokens, but several failure classes are
3
+ semantic and need your judgment.
4
+
5
+ Steps:
6
+
7
+ 1. Run the gates and fix anything they report first:
8
+ - `node node_modules/solid2-agent-kit/bin/solid2-kit.mjs check`
9
+ - `node node_modules/solid2-agent-kit/bin/solid2-kit.mjs doctor`
10
+ 2. Read the `solid-2` skill (`.cursor/skills/solid-2/SKILL.md` or
11
+ `.claude/skills/solid-2/SKILL.md`) and its final review checklist.
12
+ 3. Collect the diff under review: staged/uncommitted changes if any, otherwise the branch
13
+ diff against the default branch.
14
+ 4. Review every changed `.tsx`/`.jsx`/`.ts` file against the checklist, prioritizing what
15
+ regexes cannot see:
16
+ - reactive reads at component-body top level, or extracted into plain `const`s;
17
+ - async computations reading reactive inputs **after** the first `await`;
18
+ - effects that copy state instead of deriving it, or reading stores in the apply phase;
19
+ - `<For>` over server/refetched rows without a stable-id key function;
20
+ - `<Loading>`/`<Errored>` wrapping page chrome instead of the data slot, or async memos
21
+ passed as values (`user={user()}`) instead of accessors;
22
+ - store updates that rebuild objects/arrays instead of mutating the draft or reconciling;
23
+ - context values passed as snapshots instead of accessors/setters/stores;
24
+ - components with conditional/early returns on reactive values.
25
+ 5. Verify any API you are not certain about against the official docs mirror
26
+ (`https://v2-rebuild--solid-docs-v2.netlify.app/llms.txt`); never trust Solid 1.x or
27
+ React memory.
28
+
29
+ Report each finding as `file:line — problem — Solid 2 replacement`, then fix them. Finish
30
+ by re-running the gates and the project's typecheck.
@@ -2,5 +2,7 @@
2
2
 
3
3
  - All TSX in this project is Solid 2.0. Components run **once**; reactivity flows through signals/stores to JSX. React patterns (props destructuring, `className`, per-keystroke `onChange`, state-synced-by-effect, `.map()` lists) and Solid 1.x APIs (`createResource`, `onMount`, `solid-js/store`, `Suspense`, path setters) are bugs here.
4
4
  - Before writing TSX, read the `solid-2` skill (`.cursor/skills/solid-2/SKILL.md` for Cursor, `.claude/skills/solid-2/SKILL.md` for Claude Code): patterns, decision tables, official doc URLs. Hard rules auto-attach from `.cursor/rules/solid-2.mdc` in Cursor and live in a managed block in `CLAUDE.md` for Claude Code.
5
- - After editing TSX, run the `solid2-kit check` mechanical gate — it fails on React/Solid 1.x tokens and props destructuring. Use the project's `lint:solid` script if one exists; otherwise `npx solid2-agent-kit check` (with the kit as a devDependency) or `npx github:lightsound/solid2-agent-kit check`. Do not install `eslint-plugin-solid` (built for Solid 1.x; misreads Solid 2 idioms).
6
- - Solid 2.0 shipped recently and breaks with 1.x — never trust pre-2.0 Solid docs, tutorials, or training knowledge. Verify APIs against https://v2-rebuild--solid-docs-v2.netlify.app/llms.txt (markdown mirror of https://v2.solidjs.com/, which blocks non-browser fetchers). If the project has `@solidjs/router`, server functions, or `@solidjs/meta`, use those official pages (see the skill's official-docs index) — not React Router, Next.js, SolidStart 1.x, or Solid Router 0.x/1.x JSX `<Route>`/`<A>`. Core `action` from `solid-js` is a generator transaction; router `action`/`query` from `@solidjs/router` are POST forms and a read cache. Core `refresh(source)`, router `revalidate(key)`, and server-function `return reload(...)` are three different APIs. Pass the accessor to `isPending(user)`, not `isPending(user())`. Pass a function to `render`/`hydrate`: `render(() => <App />, root)`. Component teardown is `onSettled`, not `onCleanup`. One router instance; in-app navigation is `useNavigate` / `<a href>`, not `window.location`.
5
+ - After editing TSX, run the `solid2-kit check` mechanical gate — it fails on React/Solid 1.x tokens and props destructuring. Use the project's `lint:solid` script if one exists; otherwise `npx solid2-agent-kit check` (with the kit as a devDependency) or `npx github:lightsound/solid2-agent-kit check`. An edit-time hook may also run this gate automatically after each file edit — treat its findings as errors to fix immediately, not warnings. Do not install `eslint-plugin-solid` (built for Solid 1.x; misreads Solid 2 idioms).
6
+ - After editing `package.json`, `tsconfig*.json`, or root config files (Vite, ESLint), run `solid2-kit doctor` — it fails on React/Solid 1.x wiring: `react`/`vite-plugin-solid`/`eslint-plugin-solid`/SolidStart dependencies, 1.x `solid-js` ranges, and `jsx`/`jsxImportSource` values other than `preserve`/`@solidjs/web`.
7
+ - The kit's guidance and gate files (`.cursor/rules/solid-2.mdc`, the `solid-2` skill directories, hook entries, managed blocks in this file and `CLAUDE.md`) are guardrails, not obstacles. Never edit, weaken, bypass, or delete them to make a failure go away — fix the flagged code. If a guardrail seems wrong, stop and tell the user.
8
+ - Solid 2.0 shipped recently and breaks with 1.x — never trust pre-2.0 Solid docs, tutorials, or training knowledge. Verify APIs against https://v2-rebuild--solid-docs-v2.netlify.app/llms.txt (markdown mirror of https://v2.solidjs.com/, which blocks non-browser fetchers). If the project has `@solidjs/router`, server functions, or `@solidjs/meta`, use those official pages (see the skill's official-docs index) — not React Router, Next.js, SolidStart 1.x, or Solid Router 0.x/1.x JSX `<Route>`/`<A>`. Core `action` from `solid-js` is a generator transaction; router `action`/`query` from `@solidjs/router` are POST forms and a read cache. Core `refresh(source)`, router `revalidate(key)`, and server-function `return reload(...)` are three different APIs. Pass the accessor to `isPending(user)`, not `isPending(user())`. Pass a function to `render`/`hydrate`: `render(() => <App />, root)`. Component teardown is `onSettled`, not `onCleanup`. One router instance; in-app navigation is `useNavigate` / `<a href>`, not `window.location`. Types: `solid-js` exports no `JSX` namespace — children/return types are `Element` from `solid-js`; DOM `JSX` types come from `@solidjs/web`. Reset-a-signal-on-change is a writable derivation (`createSignal(() => { source(); return initial; })`), never an effect calling the setter. Client mode mounts with `render()` into an empty body — no hydration mismatch exists, so never defer initial values to post-mount React-style (that can overwrite persisted state).
@@ -25,7 +25,13 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
25
25
  (the function itself) into JSX and call it there.
26
26
  3. Signals are read by **calling** them: `{count()}` in JSX, not `{count}`.
27
27
  4. **Derived state is a function, not state + effect.** `const full = () => a() + b()`.
28
- Never write "a signal plus an effect that syncs it". Use `createMemo` only for expensive
28
+ Never write "a signal plus an effect that syncs it". That includes **reset**: to clear
29
+ a signal when another reactive value changes, use a writable derivation —
30
+ `createSignal(() => { source(); return initial; })` (re-runs when `source()` changes;
31
+ the setter still works) — never a `createEffect` whose apply calls the setter. The
32
+ derivation also moves the `source()` read to the read site, under its boundaries.
33
+ Mechanically enforced by `solid2-kit check` (effects whose apply only writes a local
34
+ signal). Use `createMemo` only for expensive
29
35
  computations, multiple consumers, or an equality boundary. When several writes can land
30
36
  before a flush, use the setter updater (`setCount((c) => c + 1)`); `setCount(count() + 1)`
31
37
  reads the last *committed* value and drops the other staged writes. To store a *function*
@@ -165,7 +171,9 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
165
171
  cross an async gap default to `action` + `createOptimistic`/`createOptimisticStore` —
166
172
  except reactive clients (e.g. Convex) whose subscriptions already push authoritative
167
173
  state after mutations. Compiler: `"jsxImportSource": "@solidjs/web"` (not `"solid-js"`);
168
- Vite plugin is `@solidjs/vite-plugin` (not `vite-plugin-solid`). Server vs browser:
174
+ Vite plugin is `@solidjs/vite-plugin` (not `vite-plugin-solid`) — run `solid2-kit doctor`
175
+ after touching `package.json` / tsconfig / root configs; it fails on React and Solid 1.x
176
+ wiring. Server vs browser:
169
177
  `isServer` / `isDev` from `@solidjs/web`, or `clientOnly(() => import("./Widget"))` for
170
178
  browser-only components — never `typeof window` as the SSR boundary. Default
171
179
  `clientOnly` starts loading at declaration; `{ lazy: true }` defers until first
@@ -173,7 +181,12 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
173
181
  visible content — `clientOnly` is for components that must never run on the server.
174
182
  Pass a **function** to `render` / `hydrate` / `renderToString` / `renderToStream`
175
183
  (`render(() => <App />, root)`), never `render(<App />, root)`. `hydrate` when the
176
- container already holds SSR HTML; `render` for an empty mount. `renderToString`
184
+ container already holds SSR HTML; `render` for an empty mount. Client mode (the
185
+ `@solidjs/vite-plugin` default) mounts with `render()` into an empty body — there is
186
+ **no server HTML and no hydration mismatch**, so React's mismatch defenses
187
+ ("server-safe" initial values adopted after mount, two-pass rendering) are unnecessary
188
+ and can clobber persisted state; read `localStorage` / `matchMedia` directly when
189
+ creating the signal. `renderToString`
177
190
  emits `<Loading>` fallbacks; async or lazy-route trees need
178
191
  `await renderToStream(() => <App />)` (one consumer: `pipe` / `pipeTo` / `readable`).
179
192
  When the app owns the document, emit `<HydrationScript />` once before app markup;
@@ -254,6 +267,7 @@ directory (default `src/`).
254
267
  | Never write (Solid 1.x) | Write instead (Solid 2) |
255
268
  |---|---|
256
269
  | `import ... from "solid-js/store"` or `"solid-js/web"` | stores/`merge`/`omit` from `"solid-js"`; `render`/`hydrate`/`Portal`/`Dynamic` from `"@solidjs/web"` |
270
+ | `import type { JSX } from "solid-js"` / `JSX.Element` as the children type | `solid-js` exports no `JSX` namespace: children/returns are `Element` from `"solid-js"`; DOM-specific `JSX` types (`JSX.IntrinsicElements`, `JSX.CSSProperties`) from `"@solidjs/web"` |
257
271
  | `createResource` | async `createMemo` + `<Loading>`/`<Errored>`; `refresh()`, `latest()`, `isPending()` |
258
272
  | `createEffect(fn)` (one arg), `on(...)` | `createEffect(compute, apply)`; deps belong in `compute` |
259
273
  | `onMount` | `onSettled` (return cleanup from its callback) |
@@ -55,6 +55,7 @@ Most React `useEffect` code should NOT become `createEffect`:
55
55
  | Fetch data in an effect, copy into state | `createMemo(async () => ...)` + `<Loading>` |
56
56
  | React to a user interaction | do the work in the event handler or an `action` |
57
57
  | Sync one state into another | delete the second state; derive it |
58
+ | Reset/clear state when another reactive value changes | writable derivation: `createSignal(() => { source(); return initial; })` — not an effect calling the setter |
58
59
  | Push a settled reactive value into a non-Solid system (DOM API, third-party widget, analytics, subscription) | `createEffect(compute, apply)` — this is the only real use |
59
60
  | One-time setup after mount (`onMount`) | `onSettled(() => { ...; return cleanup })` |
60
61
  | Measure DOM / observe size after paint | ref **directive factory** + `onSettled` (writes a *new* input; not an effect that copies state) |
@@ -222,6 +223,28 @@ Reads inside `apply` do not track. Extract every needed reactive value in `compu
222
223
  and reading `user.name` there *runs once* and never retriggers. Compute-phase errors can be
223
224
  intercepted with the bundle form `createEffect(compute, { effect, error })`.
224
225
 
226
+ A two-phase effect whose apply only calls a local signal setter is "state + effect" in
227
+ disguise — formally legal Solid 2, still rule 4 (`solid2-kit check` flags it). The reset
228
+ form of a writable derivation replaces it, and moves the source read to the read site
229
+ (under that site's boundaries) instead of an effect compute:
230
+
231
+ ```tsx
232
+ // WRONG — signal synced by an effect; organizer() errors bypass the read site's <Errored>
233
+ const [signInError, setSignInError] = createSignal<string | null>(null);
234
+ createEffect(
235
+ () => organizer(),
236
+ (id) => {
237
+ if (id !== null) setSignInError(null);
238
+ },
239
+ );
240
+
241
+ // CORRECT — writable derivation: resets when organizer() changes; the setter still works
242
+ const [signInError, setSignInError] = createSignal<string | null>(() => {
243
+ organizer();
244
+ return null;
245
+ });
246
+ ```
247
+
225
248
  ### Async data
226
249
 
227
250
  ```tsx
@@ -529,6 +552,24 @@ test('increments on click', () => {
529
552
  `jsxImportSource` for tests and app code is `"@solidjs/web"`, not `"solid-js"`.
530
553
  Vite plugin is `@solidjs/vite-plugin`, not `vite-plugin-solid`.
531
554
 
555
+ To turn those dev diagnostics into a hard gate, patch `console.warn` in the test setup
556
+ file to rethrow unexpected warnings — then any top-level reactive read or unowned write
557
+ an agent sneaks in fails the suite instead of scrolling by.
558
+
559
+ ## Client mode: no server HTML, no hydration reflexes
560
+
561
+ The `@solidjs/vite-plugin` default (client mode, the `bare` tier) serves an empty-body
562
+ shell and mounts with `render()`. Nothing is server-rendered and nothing hydrates, so
563
+ **React's hydration-mismatch playbook does not transfer**: do not start signals with a
564
+ "server-safe" initial value and adopt `localStorage` / `matchMedia` after mount, do not
565
+ two-pass render, and there is no `suppressHydrationWarning`. Read client sources directly
566
+ when creating state — `createSignal(storedPreference())` is correct in client mode. A
567
+ deferred "safe" initial value is worse than unnecessary: if anything persists the value
568
+ (an effect writing `localStorage`), the placeholder **overwrites the user's stored
569
+ preference** before the real value is adopted. Mismatch only becomes a concept in
570
+ start/SSR mode, where `hydrate()` mounts onto server HTML — and the Solid answer there is
571
+ `isServer` / `clientOnly`, still not React-style deferred adoption.
572
+
532
573
  ## App stack (only if the project has these packages)
533
574
 
534
575
  This kit's always-applied rules cover `solid-js` + `@solidjs/web` TSX. Official
@@ -543,8 +584,12 @@ Solid Router 0.x/1.x stand-ins.** Patterns below; fetch the matching pages from
543
584
  { "compilerOptions": { "jsx": "preserve", "jsxImportSource": "@solidjs/web" } }
544
585
  ```
545
586
 
546
- Import DOM `JSX` types from `@solidjs/web`; renderer-neutral `Component` /
547
- `ParentProps` from `solid-js`. Vite: `import solid from "@solidjs/vite-plugin"`
587
+ `solid-js` does **not** export a `JSX` namespace — `import type { JSX } from "solid-js"`
588
+ is a type error (TS2305), and `JSX.Element` as the children type is a React reflex.
589
+ Children/return types are the renderer-neutral `Element` from `solid-js`
590
+ (`children?: Element`), alongside `Component` / `ParentProps` / `Accessor` / `Setter`.
591
+ DOM-specific `JSX` types (`JSX.IntrinsicElements`, `JSX.CSSProperties`) and
592
+ `ComponentProps` come from `@solidjs/web`. Vite: `import solid from "@solidjs/vite-plugin"`
548
593
  with `start: true` / `ssr: true` / `serverFunctions: true` only when the
549
594
  project already uses start mode. Tiers are `bare` → `basic` (router) →
550
595
  `fullstack` (SSR + server functions); do not jump a tier without cause.
@@ -790,6 +835,8 @@ how to reuse. Prefer the form on the right.
790
835
  | `createMemo(() => { void save(); return x(); })` / an effect that calls an action | invoke actions from handlers, not from memos or effects |
791
836
  | `createSignal(props.value)` / `createStore(props.items)` | `createSignal(() => props.value)` / `createStore(() => props.items, fallback)` — a bare `props.x` at setup is a snapshot |
792
837
  | `createRenderEffect` / `createTrackedEffect` as the default effect | two-phase `createEffect(compute, apply)` |
838
+ | `createEffect(() => source(), () => setError(null))` to reset a signal | `createSignal(() => { source(); return null; })` — the writable derivation resets on change; a sole-setter apply is rule 4 |
839
+ | a "server-safe" initial value adopted after mount (React hydration-mismatch fix) | client mode mounts with `render()` into an empty body — no hydration, no mismatch; read `localStorage` / `matchMedia` directly at signal creation |
793
840
 
794
841
  ```tsx
795
842
  // WRONG — post-await read never subscribes; production can hang pending
@@ -909,7 +956,9 @@ always-applied rules installed alongside this skill.
909
956
  - [ ] No props destructuring; no `props.` at component-body top level.
910
957
  - [ ] Every reactive read (`signal()`, `props.x`, `store.x`) sits in JSX, a memo, an effect
911
958
  compute, or a boundary — not the component body.
912
- - [ ] No signal-synced-by-effect; derived values are functions or memos.
959
+ - [ ] No signal-synced-by-effect; derived values are functions or memos. Reset-on-change
960
+ is a writable derivation (`createSignal(() => { source(); return initial; })`), not
961
+ an effect calling the setter.
913
962
  - [ ] Event handlers wrap setters (`onClick={() => setX(...)}`), use `currentTarget`,
914
963
  and `onInput` for keystrokes. `innerHTML` is sanitized and not mixed with children.
915
964
  - [ ] Async memos are passed as accessors (`user={user}`), not `user={user()}`, so
@@ -929,6 +978,8 @@ always-applied rules installed alongside this skill.
929
978
  - [ ] Rest props via `omit`, not `{ ...props }`. Conditional `class` via objects, not string concat.
930
979
  - [ ] Input filters write back to the DOM on reject (inputs do not rewind).
931
980
  - [ ] No Solid 1.x imports or APIs (`solid-js/store`, `createResource`, `onMount`, `Suspense`, ...).
981
+ No `JSX` imported from `solid-js` — children/return types are `Element` from
982
+ `solid-js`; DOM `JSX` types come from `@solidjs/web`.
932
983
  - [ ] Inspect children with `children()`; code-split with `lazy` + `<Loading>`; reactive
933
984
  component choice with `dynamic()`. No `React.lazy`, no effects inside ref callbacks.
934
985
  - [ ] Browser-only code uses `isServer` / `clientOnly`, not `typeof window`.
@@ -944,4 +995,5 @@ always-applied rules installed alongside this skill.
944
995
  - [ ] `jsxImportSource` is `@solidjs/web`; Vite plugin is `@solidjs/vite-plugin`.
945
996
  No `@solidjs/start`, `vinxi`, `"use client"`, or Next.js imports.
946
997
  - [ ] Single return per component; no early returns on reactive conditions.
947
- - [ ] `solid2-kit check` and the project's typecheck pass.
998
+ - [ ] `solid2-kit check` and the project's typecheck pass; `solid2-kit doctor` too if
999
+ `package.json`, tsconfig, or root config files changed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "solid2-agent-kit",
3
- "version": "0.4.5",
3
+ "version": "0.8.0",
4
4
  "description": "Teach AI coding agents (Cursor, Claude Code) to write correct Solid 2.0 instead of React or Solid 1.x — always-applied rules, an agent skill, and a mechanical pattern gate.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -16,7 +16,7 @@
16
16
  ],
17
17
  "scripts": {
18
18
  "check:drift": "node scripts/check-docs-drift.mjs",
19
- "test": "node tests/check.test.mjs"
19
+ "test": "node tests/check.test.mjs && node tests/hook.test.mjs && node tests/doctor.test.mjs && node tests/init.test.mjs"
20
20
  },
21
21
  "engines": {
22
22
  "node": ">=18"