Skip to main content

Hint packs

Hint packs are the only place framework knowledge lives. They are YAML data read by generic code; deleting the hintpacks/ folder leaves a working tool that finds routes through the other sources (your --route, Graphify, Claude Code).

Every pack has an id, a kind and a short about. skillgendev loads every *.yml under the shipped hintpacks/ folder (packaged inside the wheel), then under any folders listed in config.json hintpacks.

Shipped packs​

KindPacks
foldernextjs-app, nextjs-pages, nuxt-pages, sveltekit-routes, astro-pages
routerreact-router-jsx, route-objects, route-objects-js, express, flask, django, laravel, rails, spring, aspnet-attributes, phoenix

kind: folder: folder-based routing​

id: nextjs-app
kind: folder
about: "Next.js app router (app/**/page.tsx)"
roots: [app, src/app] # folders to scan, relative to each project root
requires_any: ["next.config.js", "next.config.mjs", "next.config.ts", "package.json"]
page_files: ["page.tsx", "page.jsx", "page.ts", "page.js", "page.mdx"]
route_from: folder # folder: route is the folder path; file: route is the file path
skip_segments: ["^\\(.*\\)$", "^@"] # path parts dropped from the route (groups, slots)
skip_files: ["^_", "^api$"] # path parts that make the whole file not a page
params: # how parameter segments are written → :name
- "^\\[\\[?\\.\\.\\.(?P<name>\\w+)\\]?\\]$"
- "^\\[(?P<name>\\w+)\\]$"

With route_from: file, index_names: [index] maps those file names to their folder.

requires_any lets one repository hold several apps. Every folder that holds one of these files, at any depth, is a project root, and roots is looked for inside each. In a monorepo with admin/next.config.js and site/package.json, both admin/src/app and site/app are scanned. Without requires_any the pack applies at the repository root only.

kind: router: route declarations found with ast-grep​

id: react-router-jsx
kind: router
about: <Route path="/x" element={<Page />} />
language: tsx
globs: ["**/*.tsx", "**/*.jsx"]
rule: # any ast-grep rule
pattern: <Route path=$PATH element={<$COMPONENT $$$ />} $$$ />
captures:
path: PATH # metavariable holding the route path (a string literal)
component: COMPONENT # optional: metavariable naming the page component
prefix_from: none # reserved for nested routers

The component name is resolved to a file through the code graph, so packs never describe import syntax. When the captured text is not a plain name (Rails writes "registrations#new"), two optional keys reshape it:

component_match: "^[\"']?(?P<name>[\\w/]+)#" # regex with a `name` group
component_format: "{pascal}Controller" # {name} or {pascal}: registrations → RegistrationsController

A simpler pack, Express:

id: express
kind: router
about: "Express-style app.get('/x', handler) and router.get('/x', handler)"
language: javascript
globs: ["**/*.js", "**/*.mjs", "**/*.cjs"]
rule:
pattern: $APP.get($PATH, $$$HANDLERS)
captures:
path: PATH

ast-grep never searches dependency or build folders (node_modules, dist and the like), and a pack whose globs match no file in the repository is skipped.

kind: validator​

The pack format reserves a third kind for field constraints found with ast-grep:

id: zod-string-min
kind: validator
about: z.string().min(n) inside an object schema
language: typescript
rule: { pattern: "$FIELD: z.string().min($MIN)" }
captures: { field: FIELD, min_length: MIN }

Validator packs would only add EXTRACTED facts that Claude Code otherwise reads itself, making analysis cheaper but never required. No validator packs ship today, and the loader reads folder and router packs.

Writing a pack​

See Adding framework support for the steps and the tests each pack needs.