solid2-agent-kit 0.4.5 → 0.7.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), and `<Dynamic>` (use `dynamic()`) |
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,8 +252,24 @@ 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 ------------------------------------------------------------------
@@ -367,8 +501,59 @@ const CHECKS = [
367
501
  },
368
502
  ];
369
503
 
370
- // .ts / .tsx sources, excluding .d.ts declaration files.
371
- const SOURCE_FILE = /(?<!\.d)\.tsx?$/;
504
+ // .ts / .tsx / .jsx sources, excluding .d.ts declaration files.
505
+ const SOURCE_FILE = /(?<!\.d)\.(?:tsx?|jsx)$/;
506
+
507
+ // Blank comment interiors (preserving newlines and offsets) so banned tokens
508
+ // mentioned in comments ("migrated off createResource") do not trip the gate.
509
+ // String and template contents are preserved — some checks match string
510
+ // literals ("use client"). This is a heuristic scanner, not a JS lexer: a
511
+ // regex literal containing escaped slashes or `//` in JSX text can blank the
512
+ // remainder of that one line (false negatives only, never false positives).
513
+ function stripComments(content) {
514
+ let out = '';
515
+ let state = 'code';
516
+ for (let i = 0; i < content.length; i += 1) {
517
+ const ch = content[i];
518
+ const next = content[i + 1];
519
+ if (state === 'code') {
520
+ if (ch === '/' && (next === '/' || next === '*')) {
521
+ state = next === '/' ? 'line' : 'block';
522
+ out += ' ';
523
+ i += 1;
524
+ } else {
525
+ if (ch === "'") state = 'single';
526
+ else if (ch === '"') state = 'double';
527
+ else if (ch === '`') state = 'template';
528
+ out += ch;
529
+ }
530
+ } else if (state === 'line') {
531
+ if (ch === '\n') state = 'code';
532
+ out += ch === '\n' ? ch : ' ';
533
+ } else if (state === 'block') {
534
+ if (ch === '*' && next === '/') {
535
+ state = 'code';
536
+ out += ' ';
537
+ i += 1;
538
+ } else {
539
+ out += ch === '\n' ? ch : ' ';
540
+ }
541
+ } else {
542
+ out += ch;
543
+ if (ch === '\\') {
544
+ out += next ?? '';
545
+ i += 1;
546
+ } else if (
547
+ (state === 'single' && ch === "'") ||
548
+ (state === 'double' && ch === '"') ||
549
+ (state === 'template' && ch === '`')
550
+ ) {
551
+ state = 'code';
552
+ }
553
+ }
554
+ }
555
+ return out;
556
+ }
372
557
 
373
558
  function* walk(dir) {
374
559
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
@@ -378,36 +563,435 @@ function* walk(dir) {
378
563
  }
379
564
  }
380
565
 
566
+ // One formatted finding per pattern match: "path:line [id] message\n > source".
567
+ // Patterns run against comment-stripped content (same offsets); the quoted
568
+ // source line comes from the original file.
569
+ function fileFindings(file, relativeTo) {
570
+ const raw = readFileSync(file, 'utf8');
571
+ const content = stripComments(raw);
572
+ const lines = raw.split('\n');
573
+ const findings = [];
574
+ for (const rule of CHECKS) {
575
+ for (const match of content.matchAll(rule.pattern)) {
576
+ const lineNumber = content.slice(0, match.index).split('\n').length;
577
+ findings.push(
578
+ `${relative(relativeTo, file) || file}:${lineNumber} [${rule.id}] ${rule.message}\n > ${lines[lineNumber - 1].trim()}`,
579
+ );
580
+ }
581
+ }
582
+ return findings;
583
+ }
584
+
381
585
  function check() {
382
586
  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);
587
+ const fileArgs = positionalArgs();
588
+
589
+ let files;
590
+ if (fileArgs.length > 0) {
591
+ // Explicit file mode (used by agent hooks): check only the named sources.
592
+ files = fileArgs.map((file) => resolve(target, file)).filter((file) => SOURCE_FILE.test(file));
593
+ } else {
594
+ const srcDir = resolve(target, flagValue('--dir', 'src'));
595
+ if (!existsSync(srcDir)) {
596
+ console.error(`solid2-kit check — source directory not found: ${srcDir} (use --dir)`);
597
+ process.exit(2);
598
+ }
599
+ files = [...walk(srcDir)];
387
600
  }
388
601
 
389
602
  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()}`,
603
+ for (const file of files) {
604
+ for (const finding of fileFindings(file, target)) {
605
+ findings += 1;
606
+ console.error(finding);
607
+ }
608
+ }
609
+
610
+ if (findings > 0) {
611
+ console.error(`\nsolid2-kit check — ${findings} finding(s) in ${files.length} file(s).`);
612
+ process.exit(1);
613
+ }
614
+ console.log(`solid2-kit check — OK (${files.length} files scanned).`);
615
+ }
616
+
617
+ // --- hook -------------------------------------------------------------------
618
+ // Edit-time gate wired by `init` into the agents' hook systems. Reads the
619
+ // hook JSON payload from stdin, runs the mechanical checks on the edited
620
+ // source files, and feeds findings back through each agent's channel:
621
+ // claude — Claude Code PostToolUse: findings on stderr + exit 2 (Claude
622
+ // sees the stderr and fixes; other exit codes are ignored). Also
623
+ // handles the Stop event as a whole-project final gate: exit 2
624
+ // blocks the agent from ending its turn with violations in place.
625
+ // cursor — Cursor postToolUse: findings as {"additional_context"} JSON on
626
+ // stdout (injected into the conversation after the tool result).
627
+ // Both agents also edit files through shell commands (sed, heredocs, mv,
628
+ // codemods), which never hit the Edit/Write tools — so shell-shaped tool
629
+ // payloads are scanned for source paths mentioned in the command and those
630
+ // files are checked after the command ran.
631
+ // Hooks must never break the agent loop: malformed payloads exit 0 silently.
632
+
633
+ const WRITE_TOOL = /write|edit|replace|apply|patch/i;
634
+ const SHELL_TOOL = /shell|bash|terminal|command|exec/i;
635
+ const MAX_REPORTED = 40;
636
+
637
+ function formatReport(header, findings) {
638
+ const shown = findings.slice(0, MAX_REPORTED);
639
+ const more = findings.length - shown.length;
640
+ return `${header}\n\n${shown.join('\n')}${more > 0 ? `\n…and ${more} more finding(s).` : ''}`;
641
+ }
642
+
643
+ function isCheckableSource(path) {
644
+ return SOURCE_FILE.test(path) && !path.includes('\n') && !path.includes('node_modules');
645
+ }
646
+
647
+ function collectSourcePaths(value, found = new Set()) {
648
+ if (typeof value === 'string') {
649
+ if (isCheckableSource(value) && existsSync(value)) found.add(value);
650
+ } else if (Array.isArray(value)) {
651
+ for (const item of value) collectSourcePaths(item, found);
652
+ } else if (value && typeof value === 'object') {
653
+ for (const item of Object.values(value)) collectSourcePaths(item, found);
654
+ }
655
+ return found;
656
+ }
657
+
658
+ // Path-shaped tokens ending in a source extension, resolved against the
659
+ // command's cwd. Existence filtering keeps false positives at zero.
660
+ function collectCommandPaths(command, cwd, found = new Set()) {
661
+ if (typeof command !== 'string') return found;
662
+ for (const match of command.matchAll(/[^\s'"`;|&()<>=]+\.(?:tsx?|jsx)\b/g)) {
663
+ const token = match[0];
664
+ if (!isCheckableSource(token)) continue;
665
+ const path = resolve(cwd, token);
666
+ if (existsSync(path)) found.add(path);
667
+ }
668
+ return found;
669
+ }
670
+
671
+ // --- pre-execution guard (self-defense + proactive denial) -------------------
672
+ // Agents that keep getting blocked by a gate tend to conclude the gate is
673
+ // wrong and edit or delete it. Pre-execution hooks (Claude PreToolUse,
674
+ // Cursor preToolUse / beforeShellExecution) can deny a tool call before it
675
+ // runs, so this guard refuses:
676
+ // - edits/deletes of kit-owned guardrail files (rules, skill, commands,
677
+ // the kit itself under node_modules),
678
+ // - edits that would remove the kit's markers from hook configs or the
679
+ // AGENTS.md / CLAUDE.md managed blocks,
680
+ // - shell rewrites of those files (sed/rm/redirects — uninspectable),
681
+ // - installing dependencies doctor would flag anyway (react,
682
+ // vite-plugin-solid, eslint-plugin-solid, SolidStart, ...).
683
+ // Everything else is allowed; this gate never inspects file *content* being
684
+ // written (that is the post-edit hook's job).
685
+
686
+ const GUARDRAIL_PATH =
687
+ /\.cursor\/rules\/solid-2\.mdc|(?:\.cursor|\.claude)\/skills\/solid-2|(?:\.cursor|\.claude)\/commands\/solid2-review\.md|node_modules\/solid2-agent-kit\//;
688
+ const MARKER_FILE = /(?:\.claude\/settings\.json|\.cursor\/hooks\.json|AGENTS\.md|CLAUDE\.md)$/;
689
+ const KIT_MARKER = 'solid2-agent-kit';
690
+ const SHELL_WRITE = /\b(?:rm|mv|cp|sed|tee|truncate|unlink|chmod|perl|python[\d.]*)\b|>>?/;
691
+ const INSTALL_CMD = /\b(?:npm|pnpm|yarn|bun)\s+(?:-\S+\s+)*(?:install|add|i)\b/;
692
+
693
+ const TAMPER_MESSAGE =
694
+ '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.';
695
+
696
+ function bannedInstall(command) {
697
+ if (!INSTALL_CMD.test(command)) return null;
698
+ for (const raw of command.split(/\s+/)) {
699
+ const token = raw.replace(/^['"]|['"]$/g, '');
700
+ const name = token.replace(/(?!^)@.*$/, '');
701
+ if (name in BANNED_DEPS) return name;
702
+ }
703
+ return null;
704
+ }
705
+
706
+ function preToolGate(agent, payload) {
707
+ const deny = (agentMessage) => {
708
+ if (agent === 'claude') {
709
+ console.error(agentMessage);
710
+ process.exit(2);
711
+ }
712
+ console.log(
713
+ JSON.stringify({
714
+ permission: 'deny',
715
+ agent_message: agentMessage,
716
+ user_message: 'solid2-agent-kit guardrail blocked this action.',
717
+ }),
718
+ );
719
+ process.exit(0);
720
+ };
721
+
722
+ let toolInput = payload.tool_input;
723
+ if (typeof toolInput === 'string') {
724
+ try {
725
+ toolInput = JSON.parse(toolInput);
726
+ } catch {
727
+ toolInput = undefined;
728
+ }
729
+ }
730
+ const toolName = typeof payload.tool_name === 'string' ? payload.tool_name : '';
731
+
732
+ // Shell commands: beforeShellExecution carries `command` at the top level,
733
+ // PreToolUse(Bash) inside tool_input.
734
+ const command =
735
+ typeof payload.command === 'string'
736
+ ? payload.command
737
+ : SHELL_TOOL.test(toolName) && typeof toolInput?.command === 'string'
738
+ ? toolInput.command
739
+ : null;
740
+ if (command !== null) {
741
+ const banned = bannedInstall(command);
742
+ if (banned) {
743
+ deny(
744
+ `solid2-agent-kit guardrail: do not install "${banned}". ${BANNED_DEPS[banned]} (\`solid2-kit doctor\` fails on it.)`,
745
+ );
746
+ }
747
+ if (SHELL_WRITE.test(command)) {
748
+ if (GUARDRAIL_PATH.test(command)) deny(TAMPER_MESSAGE);
749
+ if (/(?:\.claude\/settings\.json|\.cursor\/hooks\.json)/.test(command)) {
750
+ deny(
751
+ '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
752
  );
402
753
  }
403
754
  }
755
+ process.exit(0);
404
756
  }
405
757
 
406
- if (findings > 0) {
407
- console.error(`\nsolid2-kit check — ${findings} finding(s) in ${files} file(s).`);
758
+ if (!WRITE_TOOL.test(toolName)) process.exit(0);
759
+ const filePath = typeof toolInput?.file_path === 'string' ? toolInput.file_path : toolInput?.path;
760
+ if (typeof filePath !== 'string') process.exit(0);
761
+
762
+ if (GUARDRAIL_PATH.test(filePath)) deny(TAMPER_MESSAGE);
763
+
764
+ if (MARKER_FILE.test(filePath)) {
765
+ // Editing these files is fine (users add their own hooks/sections through
766
+ // agents) as long as the kit's entries/managed blocks survive the edit.
767
+ 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.`;
768
+ if (typeof toolInput.content === 'string') {
769
+ let existing = '';
770
+ try {
771
+ existing = readFileSync(resolve(typeof payload.cwd === 'string' ? payload.cwd : process.cwd(), filePath), 'utf8');
772
+ } catch {
773
+ // New file — nothing to preserve.
774
+ }
775
+ if (existing.includes(KIT_MARKER) && !toolInput.content.includes(KIT_MARKER)) deny(removalMessage);
776
+ } else if (
777
+ typeof toolInput.old_string === 'string' &&
778
+ toolInput.old_string.includes(KIT_MARKER) &&
779
+ !String(toolInput.new_string ?? '').includes(KIT_MARKER)
780
+ ) {
781
+ deny(removalMessage);
782
+ }
783
+ }
784
+
785
+ process.exit(0);
786
+ }
787
+
788
+ // Claude Code Stop hook: the per-edit hooks catch violations file by file,
789
+ // but the agent can still end a turn with violations introduced through
790
+ // uncovered paths (git operations, scripts writing files, moved code). The
791
+ // Stop gate re-runs the whole mechanical check plus the wiring doctor and
792
+ // refuses to let the turn end while findings remain. `stop_hook_active`
793
+ // marks a turn that is already continuing because of this gate — allow it
794
+ // through then, so an agent that cannot satisfy the gate never loops forever.
795
+ function stopGate(payload) {
796
+ if (payload.stop_hook_active) process.exit(0);
797
+ const target = process.cwd();
798
+
799
+ const findings = [];
800
+ for (const dir of ['src', 'app', 'lib']) {
801
+ const abs = join(target, dir);
802
+ if (!existsSync(abs)) continue;
803
+ for (const file of walk(abs)) findings.push(...fileFindings(file, target));
804
+ }
805
+ if (existsSync(join(target, 'package.json'))) {
806
+ findings.push(...doctorFindings(target).map(({ id, message }) => `[${id}] ${message}`));
807
+ }
808
+ if (findings.length === 0) process.exit(0);
809
+
810
+ console.error(
811
+ formatReport(
812
+ 'solid2-kit — React/Solid 1.x patterns are still in the project; fix them before finishing:',
813
+ findings,
814
+ ),
815
+ );
816
+ process.exit(2);
817
+ }
818
+
819
+ function hook(agent) {
820
+ if (agent !== 'claude' && agent !== 'cursor') {
821
+ console.error('solid2-kit hook — expected agent: claude | cursor');
822
+ process.exit(2);
823
+ }
824
+
825
+ let payload;
826
+ try {
827
+ payload = JSON.parse(readFileSync(0, 'utf8'));
828
+ } catch {
829
+ process.exit(0);
830
+ }
831
+ if (!payload || typeof payload !== 'object') process.exit(0);
832
+
833
+ const event = typeof payload.hook_event_name === 'string' ? payload.hook_event_name : '';
834
+ if (agent === 'claude' && event === 'Stop') stopGate(payload);
835
+ // Pre-execution events (PreToolUse / preToolUse / beforeShellExecution)
836
+ // only run the guard — never the content check (the file has not been
837
+ // written yet, so checking would block edits based on pre-edit state).
838
+ if (/^(?:pre|before)/i.test(event)) preToolGate(agent, payload);
839
+
840
+ // afterFileEdit-style payloads carry file_path at the top level; tool-use
841
+ // payloads carry the edited path(s) inside tool_input. Only write-shaped
842
+ // and shell-shaped tools are gated, so reads/searches never are.
843
+ const files = new Set();
844
+ if (typeof payload.file_path === 'string') collectSourcePaths(payload.file_path, files);
845
+ if (payload.tool_input !== undefined) {
846
+ const toolName = typeof payload.tool_name === 'string' ? payload.tool_name : '';
847
+ let toolInput = payload.tool_input;
848
+ if (typeof toolInput === 'string') {
849
+ try {
850
+ toolInput = JSON.parse(toolInput);
851
+ } catch {
852
+ toolInput = undefined;
853
+ }
854
+ }
855
+ if (toolName === '' || WRITE_TOOL.test(toolName)) {
856
+ collectSourcePaths(toolInput, files);
857
+ } else if (SHELL_TOOL.test(toolName)) {
858
+ const cwd = typeof payload.cwd === 'string' ? payload.cwd : process.cwd();
859
+ collectCommandPaths(toolInput?.command, cwd, files);
860
+ }
861
+ }
862
+ if (files.size === 0) process.exit(0);
863
+
864
+ const findings = [];
865
+ for (const file of files) {
866
+ try {
867
+ findings.push(...fileFindings(file, process.cwd()));
868
+ } catch {
869
+ // File disappeared between the edit and the hook — nothing to check.
870
+ }
871
+ }
872
+ if (findings.length === 0) process.exit(0);
873
+
874
+ const report = formatReport(
875
+ 'solid2-kit check — React/Solid 1.x patterns in this edit; fix them now:',
876
+ findings,
877
+ );
878
+ if (agent === 'claude') {
879
+ console.error(report);
880
+ process.exit(2);
881
+ }
882
+ console.log(JSON.stringify({ additional_context: report }));
883
+ process.exit(0);
884
+ }
885
+
886
+ // --- doctor -----------------------------------------------------------------
887
+ // Project-config gate: `check` covers TSX sources, `doctor` covers the wiring
888
+ // around them — dependencies, tsconfig, and root config files — where agents
889
+ // also reach for React / Solid 1.x tooling (vite-plugin-solid,
890
+ // eslint-plugin-solid, jsxImportSource: "solid-js", ...).
891
+
892
+ const BANNED_DEPS = {
893
+ react: 'React does not belong in a Solid 2.0 project.',
894
+ 'react-dom': 'React does not belong in a Solid 2.0 project.',
895
+ next: 'Next.js does not belong in a Solid 2.0 project.',
896
+ 'vite-plugin-solid': 'Solid 1.x Vite plugin. Use @solidjs/vite-plugin.',
897
+ 'eslint-plugin-solid':
898
+ '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.',
899
+ '@solidjs/start': 'SolidStart 1.x. Solid 2 start mode lives in @solidjs/vite-plugin (start: true).',
900
+ 'solid-start': 'SolidStart 0.x. Solid 2 start mode lives in @solidjs/vite-plugin (start: true).',
901
+ 'solid-app-router': 'Pre-1.0 router. Use @solidjs/router 2 (createRouter({ routes })).',
902
+ vinxi: 'SolidStart 1.x toolchain. Solid 2 uses @solidjs/vite-plugin directly.',
903
+ };
904
+
905
+ const CONFIG_SOURCE = /\.(?:m|c)?[jt]s$/;
906
+
907
+ // Files installed by this kit that carry a version marker. When the kit
908
+ // dependency is updated but `sync` is not re-run, the installed guidance
909
+ // keeps teaching agents the older content — silent drift doctor can catch.
910
+ const GUIDANCE_FILES = ['.cursor/rules/solid-2.mdc', 'CLAUDE.md', 'AGENTS.md'];
911
+
912
+ function doctorFindings(target) {
913
+ const findings = [];
914
+ const report = (id, message) => findings.push({ id, message });
915
+
916
+ const pkg = JSON.parse(readFileSync(join(target, 'package.json'), 'utf8'));
917
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
918
+ for (const [name, message] of Object.entries(BANNED_DEPS)) {
919
+ if (name in deps) report(`dep-${name.replace(/[@/]/g, '')}`, `package.json depends on "${name}". ${message}`);
920
+ }
921
+ const solidRange = deps['solid-js'];
922
+ if (typeof solidRange === 'string' && /^[\s^~=v]*[01]\./.test(solidRange)) {
923
+ report('solid-js-version', `package.json pins solid-js "${solidRange}" — this kit teaches Solid 2.x; upgrade to ^2.`);
924
+ }
925
+
926
+ for (const entry of readdirSync(target, { withFileTypes: true })) {
927
+ if (!entry.isFile()) continue;
928
+ const path = join(target, entry.name);
929
+
930
+ if (/^tsconfig.*\.json$/.test(entry.name)) {
931
+ // tsconfig is JSONC; extract the two relevant keys with regexes.
932
+ const content = readFileSync(path, 'utf8');
933
+ const jsx = content.match(/"jsx"\s*:\s*"([^"]+)"/)?.[1];
934
+ if (jsx && jsx !== 'preserve') {
935
+ report('tsconfig-jsx', `${entry.name} sets "jsx": "${jsx}". Solid 2 compiles its own JSX: use "preserve".`);
936
+ }
937
+ const importSource = content.match(/"jsxImportSource"\s*:\s*"([^"]+)"/)?.[1];
938
+ if (importSource && importSource !== '@solidjs/web') {
939
+ report(
940
+ 'tsconfig-jsx-import-source',
941
+ `${entry.name} sets "jsxImportSource": "${importSource}". Solid 2 JSX types live in "@solidjs/web".`,
942
+ );
943
+ }
944
+ continue;
945
+ }
946
+
947
+ if (CONFIG_SOURCE.test(entry.name)) {
948
+ const content = readFileSync(path, 'utf8');
949
+ if (/['"]vite-plugin-solid['"]/.test(content)) {
950
+ report('config-vite-plugin-solid', `${entry.name} references vite-plugin-solid (Solid 1.x). Import solid from "@solidjs/vite-plugin".`);
951
+ }
952
+ if (/jsxImportSource['"]?\s*[:=]\s*['"](?:solid-js|react)['"]/.test(content)) {
953
+ report('config-jsx-import-source', `${entry.name} sets jsxImportSource to solid-js/react. Solid 2 uses "@solidjs/web".`);
954
+ }
955
+ }
956
+
957
+ if (/^(?:\.eslintrc|eslint\.config\.)/.test(entry.name)) {
958
+ const content = readFileSync(path, 'utf8');
959
+ if (content.includes('eslint-plugin-solid')) {
960
+ report('eslint-plugin-solid', `${entry.name} wires eslint-plugin-solid (Solid 1.x analyzer; false-positives on Solid 2 idioms). Remove it.`);
961
+ }
962
+ }
963
+ }
964
+
965
+ for (const guidance of GUIDANCE_FILES) {
966
+ const path = join(target, guidance);
967
+ if (!existsSync(path)) continue;
968
+ const installed = readFileSync(path, 'utf8').match(/solid2-agent-kit v(\d[\w.-]*)/)?.[1];
969
+ if (installed && installed !== VERSION) {
970
+ report(
971
+ 'stale-guidance',
972
+ `${guidance} was installed by solid2-agent-kit v${installed} but v${VERSION} is running — run \`solid2-kit sync\` so agents read current guidance.`,
973
+ );
974
+ }
975
+ }
976
+
977
+ return findings;
978
+ }
979
+
980
+ function doctor() {
981
+ const target = resolve(flagValue('--target', '.'));
982
+ if (!existsSync(join(target, 'package.json'))) {
983
+ console.error(`solid2-kit doctor — no package.json at ${target} (use --target)`);
984
+ process.exit(2);
985
+ }
986
+
987
+ const findings = doctorFindings(target);
988
+ for (const { id, message } of findings) console.error(`[${id}] ${message}`);
989
+
990
+ if (findings.length > 0) {
991
+ console.error(`\nsolid2-kit doctor — ${findings.length} finding(s). Fix the project wiring above.`);
408
992
  process.exit(1);
409
993
  }
410
- console.log(`solid2-kit check — OK (${files} files scanned).`);
994
+ console.log('solid2-kit doctor — OK (dependencies, tsconfig, root configs, and installed guidance look like Solid 2).');
411
995
  }
412
996
 
413
997
  // --- entry ------------------------------------------------------------------
@@ -420,15 +1004,23 @@ switch (command) {
420
1004
  case 'check':
421
1005
  check();
422
1006
  break;
1007
+ case 'doctor':
1008
+ doctor();
1009
+ break;
1010
+ case 'hook':
1011
+ hook(args[1]);
1012
+ break;
423
1013
  default:
424
1014
  console.log(
425
1015
  [
426
1016
  `solid2-agent-kit v${VERSION}`,
427
1017
  '',
428
1018
  '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)',
1019
+ ' solid2-kit init [--cursor] [--claude] [--no-hooks] [--target <dir>] install/update guidance + edit hooks (default: both tools)',
1020
+ ' solid2-kit sync [--cursor] [--claude] [--no-hooks] [--target <dir>] alias of init (idempotent)',
1021
+ ' solid2-kit check [--dir <srcdir>] [--target <dir>] [files...] mechanical React/Solid 1.x pattern gate (default dir: src)',
1022
+ ' solid2-kit doctor [--target <dir>] project-wiring gate: deps, tsconfig, root configs',
1023
+ ' solid2-kit hook (claude|cursor) edit-time gate for agent hooks (stdin: hook JSON payload)',
432
1024
  ].join('\n'),
433
1025
  );
434
1026
  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).
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.
6
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`.
@@ -165,7 +165,9 @@ Claude Code) for full patterns, decision tables, and official documentation URLs
165
165
  cross an async gap default to `action` + `createOptimistic`/`createOptimisticStore` —
166
166
  except reactive clients (e.g. Convex) whose subscriptions already push authoritative
167
167
  state after mutations. Compiler: `"jsxImportSource": "@solidjs/web"` (not `"solid-js"`);
168
- Vite plugin is `@solidjs/vite-plugin` (not `vite-plugin-solid`). Server vs browser:
168
+ Vite plugin is `@solidjs/vite-plugin` (not `vite-plugin-solid`) — run `solid2-kit doctor`
169
+ after touching `package.json` / tsconfig / root configs; it fails on React and Solid 1.x
170
+ wiring. Server vs browser:
169
171
  `isServer` / `isDev` from `@solidjs/web`, or `clientOnly(() => import("./Widget"))` for
170
172
  browser-only components — never `typeof window` as the SSR boundary. Default
171
173
  `clientOnly` starts loading at declaration; `{ lazy: true }` defers until first
@@ -529,6 +529,10 @@ test('increments on click', () => {
529
529
  `jsxImportSource` for tests and app code is `"@solidjs/web"`, not `"solid-js"`.
530
530
  Vite plugin is `@solidjs/vite-plugin`, not `vite-plugin-solid`.
531
531
 
532
+ To turn those dev diagnostics into a hard gate, patch `console.warn` in the test setup
533
+ file to rethrow unexpected warnings — then any top-level reactive read or unowned write
534
+ an agent sneaks in fails the suite instead of scrolling by.
535
+
532
536
  ## App stack (only if the project has these packages)
533
537
 
534
538
  This kit's always-applied rules cover `solid-js` + `@solidjs/web` TSX. Official
@@ -944,4 +948,5 @@ always-applied rules installed alongside this skill.
944
948
  - [ ] `jsxImportSource` is `@solidjs/web`; Vite plugin is `@solidjs/vite-plugin`.
945
949
  No `@solidjs/start`, `vinxi`, `"use client"`, or Next.js imports.
946
950
  - [ ] Single return per component; no early returns on reactive conditions.
947
- - [ ] `solid2-kit check` and the project's typecheck pass.
951
+ - [ ] `solid2-kit check` and the project's typecheck pass; `solid2-kit doctor` too if
952
+ `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.7.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"