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 +81 -5
- package/bin/solid2-kit.mjs +620 -28
- package/files/commands/solid2-review.md +30 -0
- package/files/shared/agents-section.md +3 -1
- package/files/shared/rules-body.md +3 -1
- package/files/skills/solid-2/SKILL.md +6 -1
- package/package.json +2 -2
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 |
|
|
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
|
-
#
|
|
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/
|
|
172
|
+
tests/ check/doctor fixtures + hook and init-merge tests (`npm test`)
|
|
97
173
|
```
|
|
98
174
|
|
|
99
175
|
## Publishing (npm)
|
package/bin/solid2-kit.mjs
CHANGED
|
@@ -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
|
|
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
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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 (
|
|
407
|
-
|
|
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(
|
|
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>]
|
|
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`)
|
|
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.
|
|
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"
|