create-pracht 0.2.6 → 0.4.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.
@@ -0,0 +1,270 @@
1
+ ---
2
+ name: add-observability
3
+ version: 1.1.0
4
+ description: |
5
+ Wire error tracking, request tracing, and Web Vitals into a pracht app.
6
+ Supports Sentry or OpenTelemetry on the server side (loader/middleware
7
+ boundaries, API routes), and client-side Web Vitals reporting via the
8
+ `web-vitals` package.
9
+ Use when asked to "add observability", "wire Sentry", "set up tracing",
10
+ "add OpenTelemetry", "monitor Web Vitals", or "track errors".
11
+ allowed-tools:
12
+ - Bash
13
+ - Read
14
+ - Write
15
+ - Edit
16
+ - Grep
17
+ - Glob
18
+ - AskUserQuestion
19
+ ---
20
+
21
+ # Pracht Add Observability
22
+
23
+ Three layers, each opt-in:
24
+
25
+ 1. **Server error tracking** — capture loader/middleware/API exceptions.
26
+ 2. **Request tracing** — span per request with child spans per loader/db call.
27
+ 3. **Web Vitals (LCP/CLS/INP/FCP/TTFB)** — client-side, posted to a beacon
28
+ endpoint.
29
+
30
+ If the pracht MCP server is registered (see docs/MCP.md), prefer its tools
31
+ (`inspect_routes`, `inspect_api`, `inspect_build`, `doctor`, `verify`,
32
+ `generate_*`) over shelling out. Prerequisite: `pracht inspect` needs a vite
33
+ config with the pracht plugin registered.
34
+
35
+ ## Step 1: Pick the stack
36
+
37
+ Use `AskUserQuestion`:
38
+
39
+ - **Sentry** — easiest end-to-end (errors + traces + Web Vitals).
40
+ - **OpenTelemetry + your backend** (Honeycomb, Grafana, Datadog, Jaeger).
41
+ - **Custom beacon** — minimal `fetch('/api/telemetry')` setup, no SaaS.
42
+
43
+ The skill below shows Sentry and OTel patterns. Custom beacon is mentioned
44
+ but trivial.
45
+
46
+ ## Step 2: Server error tracking
47
+
48
+ ### Sentry path (Node adapter)
49
+
50
+ The pattern below uses `@sentry/node` and works on the **Node adapter only**
51
+ — see the caveat box below for Cloudflare and Vercel Edge before installing
52
+ anything.
53
+
54
+ ```bash
55
+ pnpm add @sentry/node # Node adapter only
56
+ ```
57
+
58
+ Create `src/server/observability.ts`:
59
+
60
+ ```ts
61
+ import { serverEnv } from "@pracht/core/env/server";
62
+ import * as Sentry from "@sentry/node";
63
+
64
+ let initialized = false;
65
+ export function initObservability() {
66
+ if (initialized) return;
67
+ initialized = true;
68
+ // serverEnv is read INSIDE this function, not at module scope — module-level
69
+ // env reads break on runtimes where env arrives per request (docs/ENV.md).
70
+ Sentry.init({
71
+ dsn: serverEnv.SENTRY_DSN,
72
+ tracesSampleRate: Number(serverEnv.SENTRY_TRACES_SAMPLE_RATE ?? 0.1),
73
+ environment: serverEnv.NODE_ENV,
74
+ });
75
+ }
76
+ ```
77
+
78
+ Add a global middleware that calls `initObservability()` once (from inside
79
+ the handler, never at module scope) and wraps the downstream call:
80
+
81
+ ```ts
82
+ // src/middleware/observability.ts
83
+ import type { MiddlewareFn } from "@pracht/core";
84
+ import * as Sentry from "@sentry/node";
85
+ import { initObservability } from "../server/observability";
86
+
87
+ export const middleware: MiddlewareFn = async ({ request, route }, next) => {
88
+ initObservability();
89
+ return Sentry.startSpan(
90
+ {
91
+ name: `${request.method} ${route.path}`,
92
+ op: "http.server",
93
+ },
94
+ () => next(),
95
+ );
96
+ };
97
+ ```
98
+
99
+ > **Cloudflare / Vercel Edge caveat — be honest here.** `@sentry/cloudflare`
100
+ > requires wrapping the worker's fetch handler with `withSentry()`, but
101
+ > pracht's Cloudflare adapter owns that handler — there is no user hook to
102
+ > wrap it today, so the middleware-init pattern above **cannot work** with
103
+ > `@sentry/cloudflare`, and `@sentry/node` does not run on Workers at all.
104
+ > Do not scaffold a pattern that can't work. Options on Cloudflare:
105
+ > 1. Plain fetch-based event forwarding: catch errors in a wrap-around
106
+ > middleware and `fetch` them to Sentry's store/envelope endpoint (or any
107
+ > HTTP sink) yourself. Read the DSN via `serverEnv` inside the middleware.
108
+ > 2. Wait for pracht to expose a handler-wrap hook for the adapter, then use
109
+ > Sentry's Cloudflare SDK properly.
110
+ >
111
+ > The same applies to `@sentry/vercel-edge`: verify how the init hooks into
112
+ > the runtime before installing; if it needs to own the handler, fall back to
113
+ > option 1. (This mirrors the OTel-edge honesty note below.)
114
+
115
+ Pracht middleware is wrap-around: `await next()` invokes the rest of the
116
+ request and resolves to the final `Response`, so the span naturally covers
117
+ the loader/handler and ends when they finish.
118
+
119
+ Register it in `defineApp({ middleware: { observability: "./..." } })` (the
120
+ top-level `middleware` field is a *registry* keyed by name — not an ordered
121
+ chain). To actually wrap requests, place `"observability"` first in every
122
+ chain that should cover them:
123
+
124
+ ```ts
125
+ defineApp({
126
+ middleware: { observability: "./middleware/observability.ts", auth: "./middleware/auth.ts" },
127
+ api: { middleware: ["observability"] }, // all API routes
128
+ routes: [
129
+ group({ middleware: ["observability"] }, [ // all pages
130
+ group({ middleware: ["auth"] }, [ /* protected routes */ ]),
131
+ ]),
132
+ ],
133
+ });
134
+ ```
135
+
136
+ Ordering lives in these `middleware: [...]` arrays — always place
137
+ observability first so it spans the rest of the chain.
138
+
139
+ ### OpenTelemetry path
140
+
141
+ ```bash
142
+ pnpm add @opentelemetry/api @opentelemetry/sdk-trace-node @opentelemetry/auto-instrumentations-node
143
+ ```
144
+
145
+ Create a SDK init module that runs at server entry — for Node, use the
146
+ `--require ./otel.cjs` flag; for Cloudflare/Vercel edge, OTel is more limited
147
+ (use HTTP exporter directly). Surface this trade-off; don't pretend OTel
148
+ edge is plug-and-play.
149
+
150
+ ## Step 3: Loader/API tracing
151
+
152
+ For each loader and API handler, wrap the body in a span.
153
+
154
+ ```ts
155
+ import * as Sentry from "@sentry/node";
156
+
157
+ export async function loader({ request }) {
158
+ return Sentry.startSpan({ name: "loader: dashboard", op: "function" }, async () => {
159
+ return { /* ... */ };
160
+ });
161
+ }
162
+ ```
163
+
164
+ Auto-injection is out of scope; provide a snippet, recommend wrapping the 5-10
165
+ slowest loaders (cross-reference with `audit-bundles` perf hotspots).
166
+
167
+ ## Step 4: Web Vitals on the client
168
+
169
+ ```bash
170
+ pnpm add web-vitals
171
+ ```
172
+
173
+ Create `src/client/vitals.ts` — export a function, **no module-level
174
+ side effects**:
175
+
176
+ ```ts
177
+ import { onCLS, onINP, onLCP, onFCP, onTTFB, type Metric } from "web-vitals";
178
+
179
+ function send(metric: Metric) {
180
+ navigator.sendBeacon?.(
181
+ "/api/telemetry/vitals",
182
+ JSON.stringify({ name: metric.name, value: metric.value, id: metric.id, path: location.pathname }),
183
+ );
184
+ }
185
+
186
+ export function reportVitals() {
187
+ onCLS(send);
188
+ onINP(send);
189
+ onLCP(send);
190
+ onFCP(send);
191
+ onTTFB(send);
192
+ }
193
+ ```
194
+
195
+ Do NOT import this statically from a shell: shells render on the **server**
196
+ too, so module-level `onCLS(...)` calls would execute during SSR. The primary
197
+ pattern is a lazy `import()` inside an effect, guarded by `useIsHydrated`
198
+ (exported from `@pracht/core`), placed in a shell or top-level component:
199
+
200
+ ```tsx
201
+ import { useIsHydrated } from "@pracht/core";
202
+ import { useEffect } from "preact/hooks";
203
+
204
+ export function Vitals() {
205
+ const hydrated = useIsHydrated();
206
+ useEffect(() => {
207
+ if (!hydrated) return;
208
+ void import("../client/vitals").then((m) => m.reportVitals());
209
+ }, [hydrated]);
210
+ return null;
211
+ }
212
+ ```
213
+
214
+ This keeps `web-vitals` out of the critical bundle (lazy chunk) and only
215
+ starts observers after hydration has fully settled.
216
+
217
+ ## Step 5: Beacon endpoint
218
+
219
+ ```ts
220
+ // src/api/telemetry/vitals.ts
221
+ import type { ApiRouteArgs } from "@pracht/core";
222
+
223
+ export async function POST({ request }: ApiRouteArgs) {
224
+ const body = await request.text();
225
+ // Forward to your destination (Sentry, Honeycomb, custom store).
226
+ // Keep body small; do not block on the upstream.
227
+ console.log("vitals", body);
228
+ return new Response(null, { status: 204 });
229
+ }
230
+ ```
231
+
232
+ For Sentry users, Sentry's browser SDK can capture Web Vitals natively —
233
+ prefer that over a custom beacon if you've gone the Sentry route.
234
+
235
+ ## Step 6: Sampling and PII
236
+
237
+ - Set `SENTRY_TRACES_SAMPLE_RATE` to a small number (0.05–0.10) in
238
+ production.
239
+ - Scrub auth headers and cookies from breadcrumbs:
240
+ ```ts
241
+ Sentry.init({ beforeSend(event) { delete event.request?.headers?.cookie; return event; } });
242
+ ```
243
+ - Never send loader return values verbatim — they often contain user data.
244
+
245
+ ## Step 7: Verify
246
+
247
+ - Trigger a deliberate error in dev and confirm it lands in Sentry/OTel.
248
+ - Open a route, check the Web Vitals beacon fires (Network tab).
249
+ - Confirm `pnpm test` and `pnpm e2e` still pass.
250
+ - Run `pracht typegen` if any routes were added (the beacon API route does
251
+ not affect page-route types, but re-run when in doubt).
252
+ - Run `pracht verify --json` and confirm no failures.
253
+
254
+ ## Rules
255
+
256
+ 1. Confirm adapter compatibility before installing the SDK package
257
+ (Sentry has separate packages per runtime), and never scaffold a pattern
258
+ the runtime can't actually run — see the Cloudflare/Vercel-edge caveat in
259
+ Step 2. Read `SENTRY_DSN` and friends via `serverEnv` inside functions,
260
+ never `process.env` at module scope.
261
+ 2. Top-level `middleware` in `defineApp` is a name→path *registry*, not an
262
+ ordered chain. Place `"observability"` first in every `group({
263
+ middleware: [...] })` and in `api.middleware` so it wraps the rest.
264
+ 3. Web Vitals only matter for SSR/SSG/ISG routes that hydrate; SPA-only
265
+ routes still benefit but the values reflect the post-bootstrap state.
266
+ 4. Sample traces (≤ 10%) in production; full sampling in dev.
267
+ 5. Never send raw cookies, auth headers, or full loader payloads to a
268
+ third-party SaaS.
269
+
270
+ $ARGUMENTS
@@ -0,0 +1,170 @@
1
+ ---
2
+ name: audit-a11y
3
+ version: 1.1.0
4
+ description: |
5
+ Per-route accessibility audit for a pracht app. Drives a headless browser
6
+ through every route in the manifest, runs axe-core, and reports issues
7
+ grouped by severity and route. Catches alt-text gaps, contrast failures,
8
+ missing landmarks, focus-order bugs, and form-label problems.
9
+ Use when asked to "audit a11y", "check accessibility", "axe my app",
10
+ "WCAG compliance", or "screen reader test".
11
+ allowed-tools:
12
+ - Bash
13
+ - Read
14
+ - Write
15
+ - Grep
16
+ - Glob
17
+ - AskUserQuestion
18
+ ---
19
+
20
+ # Pracht Audit A11y
21
+
22
+ Static linting cannot prove a route is accessible — many issues only appear
23
+ in the rendered DOM. This skill renders each route in a real browser and
24
+ runs axe-core against the result.
25
+
26
+ ## Step 1: Boot the app
27
+
28
+ Prefer the production build/runtime — production HTML is what users actually
29
+ receive. `pracht preview` builds and serves it for **Node and Cloudflare**
30
+ targets. It refuses Vercel targets (it prints guidance and exits nonzero):
31
+ for a Vercel app, run `vercel dev` yourself or point `BASE_URL` at a deployed
32
+ preview instead. Fall back to `pracht dev` only if the user can't build.
33
+
34
+ Run the server as a managed background process, wait for readiness, and clean
35
+ it up when the audit ends:
36
+
37
+ ```bash
38
+ pracht preview & # run in the background (use the Bash tool's background mode)
39
+ # wait for readiness before auditing:
40
+ until curl -sf http://localhost:3000 > /dev/null; do sleep 1; done
41
+ ```
42
+
43
+ After Step 5 (or on any failure), kill the background process — do not leave
44
+ a stray server holding the port.
45
+
46
+ Or, if `BASE_URL` is set, target the deployed app and skip the local server
47
+ entirely.
48
+
49
+ ## Step 2: Install runner
50
+
51
+ If Playwright is already wired (see `scaffold-e2e`), reuse it. Install the
52
+ axe adapter plus `tsx` (the Step 5 runner — it is not a pracht dependency):
53
+
54
+ ```bash
55
+ pnpm add -D @axe-core/playwright tsx
56
+ ```
57
+
58
+ If Playwright is not wired, scaffold a one-off script using `playwright`
59
+ directly. Prefer Playwright over Puppeteer for consistency with existing
60
+ project tooling.
61
+
62
+ ## Step 3: Enumerate routes
63
+
64
+ If the pracht MCP server is registered (see docs/MCP.md), prefer its tools
65
+ (`inspect_routes`, `inspect_api`, `inspect_build`, `doctor`, `verify`) over
66
+ shelling out.
67
+
68
+ ```bash
69
+ pracht inspect routes --json
70
+ ```
71
+
72
+ Prerequisite: `pracht inspect` needs a vite config with the pracht plugin
73
+ wired up.
74
+
75
+ For dynamic-segment routes, ask the user once for example params (or skip
76
+ with a note in the report).
77
+
78
+ ## Step 4: Generate the audit script
79
+
80
+ `scripts/audit-a11y.ts`:
81
+
82
+ ```ts
83
+ import { chromium } from "playwright";
84
+ import AxeBuilder from "@axe-core/playwright";
85
+
86
+ const BASE = process.env.BASE_URL ?? "http://localhost:3000";
87
+ const ROUTES: string[] = [/* injected from pracht inspect routes */];
88
+
89
+ const browser = await chromium.launch();
90
+ const context = await browser.newContext();
91
+ const page = await context.newPage();
92
+
93
+ const results = [];
94
+ for (const path of ROUTES) {
95
+ await page.goto(`${BASE}${path}`, { waitUntil: "networkidle" });
96
+ const axe = await new AxeBuilder({ page })
97
+ .withTags(["wcag2a", "wcag2aa", "wcag21aa", "best-practice"])
98
+ .analyze();
99
+ results.push({ path, violations: axe.violations });
100
+ }
101
+
102
+ await browser.close();
103
+ console.log(JSON.stringify(results, null, 2));
104
+ ```
105
+
106
+ ## Step 5: Run and aggregate
107
+
108
+ ```bash
109
+ pnpm exec tsx scripts/audit-a11y.ts > a11y-report.json
110
+ ```
111
+
112
+ Then stop the background server from Step 1.
113
+
114
+ Aggregate by:
115
+
116
+ - **Per-route summary**: violation count, worst severity.
117
+ - **Per-rule summary**: which rule fires most across the app — usually
118
+ reveals a single component (header, footer, form) that authors every page.
119
+
120
+ ## Step 6: Report
121
+
122
+ Report findings with a primary severity of `error` / `warn` / `info`,
123
+ mapping axe impacts: critical + serious → `error`, moderate → `warn`,
124
+ minor → `info`. Keep the raw axe impact as a secondary column.
125
+
126
+ ```
127
+ ## Per-route summary
128
+
129
+ | Route | Critical | Serious | Moderate | Minor |
130
+ | ----------- | -------- | ------- | -------- | ----- |
131
+ | / | 0 | 1 | 2 | 0 |
132
+
133
+ ## Per-rule summary (top offenders)
134
+
135
+ - `color-contrast` — 12 violations across 8 routes
136
+ → Likely source: shared Button component (src/components/Button.tsx)
137
+ - `image-alt` — 5 violations across 3 routes
138
+ → Likely source: <img> in shells/marketing.tsx
139
+
140
+ ## Detail
141
+
142
+ [per-route violations with selectors and help URLs]
143
+ ```
144
+
145
+ ## Step 7: Targeted fixes
146
+
147
+ For the top 3 issues, propose concrete patches:
148
+
149
+ - `color-contrast` → suggest tokenized color pairs from existing CSS vars.
150
+ - `image-alt` → list every `<img>` missing `alt` and propose either a
151
+ description or `alt=""` (decorative).
152
+ - `landmark-one-main` → confirm shells render exactly one `<main>` element.
153
+ - `label` → list inputs without associated `<label>` and propose
154
+ `htmlFor`/`for` or wrapping pattern.
155
+
156
+ ## Rules
157
+
158
+ 1. Run against the production build/runtime if at all possible — minified production
159
+ markup is what real users hit.
160
+ 2. axe with WCAG 2.1 AA + best-practice tags is the default; ask before
161
+ downgrading.
162
+ 3. Aggregate by rule before per-route — the same component is usually
163
+ responsible for most violations.
164
+ 4. Do not auto-fix. Suggest, then let the user review per component.
165
+ 5. For SPA routes that need interaction before content appears, ask the user
166
+ for a setup hook (e.g., a script that logs in and lands on the dashboard).
167
+ 6. Always clean up background servers you started, even when the audit fails
168
+ partway.
169
+
170
+ $ARGUMENTS
@@ -0,0 +1,144 @@
1
+ ---
2
+ name: audit-auth
3
+ version: 1.1.0
4
+ description: |
5
+ Find pracht routes that look protected but aren't — missing auth middleware,
6
+ middleware that augments context but never gates, client-side auth checks
7
+ with no server enforcement, and API mutations exposed without guards.
8
+ Use when asked to "audit auth", "check route protection", "is my dashboard
9
+ protected", "find unauthenticated routes", or "review middleware coverage".
10
+ allowed-tools:
11
+ - Bash
12
+ - Read
13
+ - Grep
14
+ - Glob
15
+ ---
16
+
17
+ # Pracht Audit Auth
18
+
19
+ What the framework guarantees: middleware runs wrap-around style — every
20
+ middleware must return a `Response` (the runtime throws if it doesn't), either
21
+ `return next()` to continue the chain or a short-circuit `Response` to stop it.
22
+ The `redirect()` helper from `@pracht/core` returns a scheme/CRLF-validated
23
+ redirect `Response`. What the framework does NOT decide is *which* routes get
24
+ an auth gate — that is app wiring, and this skill audits it.
25
+
26
+ The pracht auth pattern (see `examples/docs/src/routes/docs/recipes-auth.md`):
27
+ middleware checks the session, short-circuits with a redirect on absence, and
28
+ forwards user info via request headers; loaders downstream read the headers.
29
+
30
+ Prerequisites: `pracht inspect` requires a vite config that registers the
31
+ pracht plugin.
32
+
33
+ ## Step 1: Identify the auth middleware(s)
34
+
35
+ ```bash
36
+ pracht inspect routes --json
37
+ ```
38
+
39
+ If the pracht MCP server is registered (see `docs/MCP.md`), prefer its tools
40
+ (`inspect_routes`, `inspect_api`, `inspect_build`, `doctor`, `verify`) over
41
+ shelling out.
42
+
43
+ Middleware is registered by name in the app manifest —
44
+ `defineApp({ middleware: { auth: () => import("./middleware/auth.ts") } })` —
45
+ and `inspect` reports those names, not files. Read the name→file map from
46
+ `src/routes.ts` (or the configured manifest) to resolve each name, then read
47
+ each middleware file and classify it:
48
+
49
+ - **Gate** — on auth failure, returns a short-circuit `Response`
50
+ (`redirect("/login", { request })`, or a 401/403 `Response`) WITHOUT calling
51
+ `next()`; on success, `return next()`.
52
+ - **Augmenter** — mutates request headers/context with user info, then always
53
+ returns `next()`. Never short-circuits.
54
+ - **Other** — non-auth middleware (rate limit, logging, CORS, etc.).
55
+
56
+ The "Augmenter" category is the silent killer: it makes loaders *think*
57
+ auth is enforced because `request.headers.get('x-user-id')` returns a value
58
+ when present, but unauthenticated requests just get `null` and the loader has
59
+ to handle it. Flag every loader downstream of an Augmenter that doesn't.
60
+
61
+ ## Step 2: Identify protected routes
62
+
63
+ A route is "expected protected" if any of:
64
+
65
+ - It has `auth`/`session`/`requireUser`/similar middleware applied.
66
+ - Its loader reads `x-user-id`/`x-user-email`/`getSession`/equivalent.
67
+ - It lives under conventional protected paths: `/dashboard*`, `/admin*`,
68
+ `/account*`, `/settings*`, `/app*` (ask the user to confirm the
69
+ convention if unclear).
70
+ - The user has flagged it explicitly.
71
+
72
+ Build a list of expected-protected routes.
73
+
74
+ ## Step 3: Check coverage per protected route
75
+
76
+ For each expected-protected route:
77
+
78
+ 1. From `pracht inspect routes --json`, read the resolved `middleware` array.
79
+ 2. Confirm at least one **Gate** middleware is present.
80
+ 3. Confirm the gate runs **before** any other middleware that depends on
81
+ identity (order matters).
82
+ 4. If only an Augmenter is present, mark as `augmented-only`.
83
+
84
+ ## Step 4: Check the API surface
85
+
86
+ Mutation endpoints (`POST`, `PUT`, `PATCH`, `DELETE`) are the highest-impact
87
+ target. From `pracht inspect api --json`:
88
+
89
+ - Each API route reports `path`, `file`, `methods`, and `hasDefaultHandler`
90
+ (the last requires a current `@pracht/cli`). A `default`-export handler
91
+ serves ALL methods but reports `methods: []` — treat
92
+ `hasDefaultHandler: true` as "every method exposed". On older CLIs where
93
+ the field is missing, grep the handler file for `export default` instead.
94
+ - For each mutation handler (named method export or default handler), check
95
+ whether `defineApp({ api: { middleware } })` applies a Gate, OR the handler
96
+ reads/validates a session itself.
97
+ - Common bug: dashboard route is protected by middleware, but
98
+ `POST /api/items` is not — attacker bypasses the UI entirely.
99
+
100
+ ## Step 5: Client/server enforcement parity
101
+
102
+ Grep client components for patterns like `if (!user) return <Login />`. For
103
+ each occurrence, confirm that **the data path is also gated server-side**.
104
+ Client-side gating without a server gate is purely cosmetic and a common
105
+ source of "I see the data flash before redirect" or worse, leaked data via
106
+ SPA route loaders.
107
+
108
+ ## Step 6: Session cookie sanity
109
+
110
+ Cross-reference with `audit-csrf`: the same cookies that authorize the user
111
+ are the CSRF target. Recommend running `audit-csrf` after this skill.
112
+
113
+ ## Step 7: Report
114
+
115
+ | Route/API | Expected | Resolved middleware | Gate present? | Severity | Verdict |
116
+ | --------- | -------- | ------------------- | ------------- | -------- | ------- |
117
+
118
+ Severity is the primary scale; the verdict is a secondary domain label:
119
+
120
+ - `error` / `unprotected` — no auth middleware on a route the user expects
121
+ protected.
122
+ - `error` / `inconsistent` — UI route is gated; sibling API is not.
123
+ - `warn` / `augmented-only` — middleware reads session but never blocks;
124
+ loader must handle null user.
125
+ - `warn` / `client-only` — server allows; client hides UI.
126
+ - `info` / `protected` — gate confirmed.
127
+ - `info` / `public-by-design` — deliberately exposed (login, signup,
128
+ marketing).
129
+
130
+ ## Rules
131
+
132
+ 1. The framework's `pracht inspect routes --json` and `pracht inspect api
133
+ --json` are the source of truth — group inheritance is already resolved.
134
+ 2. Recognize Gates by behavior (short-circuits with a `Response` without
135
+ calling `next()` on failure), not by filename — projects use `auth.ts`,
136
+ `requireUser.ts`, `session.ts`, etc.
137
+ 3. An Augmenter is a valid pattern when paired with a separate Gate or a
138
+ loader that explicitly handles the unauthenticated case. Flag it; don't
139
+ condemn it.
140
+ 4. Public routes deliberately exposed (login, signup, marketing) should be
141
+ listed but not flagged.
142
+ 5. Do not auto-add middleware. Auth wiring is policy.
143
+
144
+ $ARGUMENTS