🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP β†’
Sign In

@silverassist/agents-toolkit

Package Overview
Dependencies
Maintainers
1
Versions
8
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@silverassist/agents-toolkit - npm Package Compare versions

Comparing version
2.4.0
to
2.5.0
+121
templates/shared/instructions/caching.instructions.md
---
applyTo: "**/next.config.*,**/src/proxy.ts,**/src/lib/**/*.ts,**/src/app/**/route.ts,**/src/app/**/page.tsx"
---
# Caching Standards (CRITICAL)
These rules are **mandatory** when touching data fetching, route segment configs, the asset/page
proxy, image config, or the on-demand revalidate route. Caching has several **independent layers**
(ISR page cache, data-fetch cache, request dedup, asset proxy, edge/CDN) β€” fixing one does not fix
the others.
> Apply to any Silver Side Next.js (App Router) frontend consuming the CCDS API and/or WordPress
> (headless GraphQL). If the project keeps a `docs/CACHING.md`, treat it as the extended reference.
## Hard rules
1. **Cache reads, not mutations β€” decide on intent, NEVER on the HTTP method.**
Some reads must use `POST` because they take a body (e.g. CCDS `geo-search`). They still have to
cache like a `GET`. Mutations (`submit-review`, lead submit, and any `PUT`/`DELETE`/`PATCH`) must
stay uncached with `cache: "no-store"`. Expose a `mutation: true` flag on the client and mark every
write with it.
- ❌ **Never** gate caching on `method === "GET"` (e.g. `next: method === "GET" ? {...} : { revalidate: 0 }`).
That leaves POST reads uncached and forces the whole route into dynamic rendering
(`cache-control: private, no-store`).
2. **A `POST` IS cacheable cross-request β€” but only with explicit `next` options.**
Next.js does not cache `POST` by default. Any read `fetch` to CCDS or the WP GraphQL endpoint β€”
GET or POST β€” must set `next: { revalidate, tags }`. The request body is part of the cache key, so
different filter bodies cache independently.
3. **Always pair `tags` with a `revalidate` duration.** `next: { tags }` alone either holds stale data
indefinitely or (Next 16 default) does not cache at runtime at all. Use a `CACHE_DURATIONS`-style
default (e.g. 24h) so freshness survives a missed webhook.
4. **`React.cache()` is request dedup only.** It collapses duplicate calls within a single render. It
does **not** cache across requests and is never a substitute for `next: { revalidate }`.
5. **Every cacheable route exports `revalidate`.** Suggested tiers: listing 24h–30d, detail/city 24h
(`86400`), state/landing 7d (`604800`), WP catch-all 7d. Do **not** add `revalidate` to
mutation/personalized routes (forms, thank-you, wizards). Prefer `export const revalidate` over
`dynamic = "force-static"` so a failed ISR revalidation preserves the last good cache instead of
overwriting it with a broken page.
6. **On-demand revalidation dual-invalidates.** `/api/revalidate` must call `revalidateTag`/
`revalidatePath` **and** the CDN invalidation (e.g. `invalidateCloudFrontPaths`) in the same
request, otherwise the CDN keeps serving stale until its own TTL.
7. **Image config** in `next.config`: set `minimumCacheTTL: 2592000` (30d), `qualities`, and
`formats: ["image/avif", "image/webp"]`. No malformed `remotePatterns` hostnames.
8. **Asset proxy TTLs** are type-keyed (image/css/font) at `365d + SWR 30d`.
## Canonical snippet β€” API client (read vs. mutation)
```ts
interface FetchOptions {
endpoint: string;
method?: "GET" | "POST" | "PUT" | "DELETE";
body?: object | null;
revalidateTag?: string;
revalidate?: number;
/** Writes are never cached. Reads (default) cache regardless of method. */
mutation?: boolean;
}
export async function fetchData<T>({
endpoint,
method = "GET",
body = null,
revalidateTag,
revalidate = 86400, // 24h time-based fallback; pair with tags for surgical invalidation
mutation = false,
}: FetchOptions): Promise<T | null> {
const isMutation = mutation || method === "PUT" || method === "DELETE";
const requestOptions: RequestInit = {
method,
// Reads (GET or POST) cache cross-request via `next`; mutations opt out.
...(isMutation
? { cache: "no-store" as RequestCache }
: { next: { revalidate, tags: revalidateTag ? [revalidateTag] : [] } }),
};
if ((method === "POST" || method === "PUT") && body) {
requestOptions.body = JSON.stringify(body);
}
// ...fetch + error handling...
}
```
## Canonical snippet β€” WordPress GraphQL client (cached POST)
```ts
export const WP_CACHE_DURATIONS = {
pages: 86400, posts: 86400, menus: 86400, staticPages: 604800, default: 86400,
} as const;
export async function fetchWPAPI<T>(
query: string,
{ variables, revalidate = WP_CACHE_DURATIONS.default, tags }: {
variables?: Record<string, unknown>; revalidate?: number; tags?: string[];
} = {},
) {
const res = await fetch(WP_API_URL, {
method: "POST",
headers,
body: JSON.stringify({ query, variables }),
next: { revalidate, ...(tags && { tags }) }, // a POST IS cacheable WITH next options
});
}
```
## Do NOT
- ❌ `next: method === "GET" ? {...} : { revalidate: 0 }` β€” leaves POST reads uncached.
- ❌ `fetch(API_URL, { method: "POST", body })` with no `next` options (bare, uncached POST read).
- ❌ `next: { tags }` with no `revalidate`.
- ❌ Adding `export const revalidate` to a form/mutation/personalized route.
- ❌ Treating `React.cache()` as cross-request caching.
- ❌ Caching a mutation (`submit-review`, lead submit, `PUT`/`DELETE`/`PATCH`).
- ❌ Mixing `cacheComponents: true` (`"use cache"`) with route-segment `export const revalidate` β€”
that is a separate, deliberate migration; do not introduce it ad hoc.
---
applyTo: "**/*.tsx"
---
# SEO & AI Optimization Patterns
These rules ensure components are optimized for Google's generative AI features (AI Overviews, AI
Mode) and browser-agent interactions: agent-friendly HTML, a clean accessibility tree, semantic
structure, and E-E-A-T signals. Pairs with the `ai-seo-optimization` skill and the `audit-ai-seo`
prompt.
---
## Semantic HTML (CRITICAL)
### Interactive Elements
**NEVER use `<div>` or `<span>` with click handlers. Use proper semantic elements.**
```tsx
// βœ… CORRECT: Semantic button
<button onClick={handleClick} type="button">
Click me
</button>
// βœ… CORRECT: Navigation link
<Link href="/page">Go to page</Link>
// ❌ WRONG: Non-semantic clickable div
<div onClick={handleClick} className="cursor-pointer">
Click me
</div>
// ❌ WRONG: Span with role hack
<span role="button" onClick={handleClick}>
Click me
</span>
```
### Form Inputs
**Every input MUST have an associated label. Placeholder is NOT a substitute.**
```tsx
// βœ… CORRECT: Label with htmlFor
<label htmlFor="email">Email address</label>
<input id="email" name="email" type="email" required aria-required="true" />
// βœ… CORRECT: Wrapped label (implicit association)
<label>
Email address
<input name="email" type="email" required />
</label>
// ❌ WRONG: Placeholder only (invisible to accessibility tree)
<input placeholder="Enter your email" type="email" />
// ❌ WRONG: aria-label without visible label (poor for agents)
<input aria-label="Email" type="email" />
```
### Heading Hierarchy
**Headings must follow logical order. Never skip levels.**
```tsx
// βœ… CORRECT: Proper hierarchy
<h1>Page Title</h1>
<section>
<h2>Section Title</h2>
<h3>Subsection</h3>
</section>
// ❌ WRONG: Skipping h2
<h1>Page Title</h1>
<h3>Subsection</h3>
// ❌ WRONG: Using heading for styling only
<h4 className="text-sm font-bold">Small bold text</h4> // Use <p> with classes instead
```
### Lists
```tsx
// βœ… CORRECT: Semantic list
<ul>
{items.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
// ❌ WRONG: Div-based list
<div className="flex flex-col gap-2">
{items.map(item => <div key={item.id}>{item.name}</div>)}
</div>
```
---
## Accessibility for AI Agents
### Skip-to-Content Link
The root layout MUST include a skip-to-content link as the first focusable element:
```tsx
<body>
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:absolute focus:z-50 focus:p-4 focus:bg-background focus:text-foreground"
>
Skip to main content
</a>
{/* Header/Navigation */}
<main id="main-content">
{children}
</main>
</body>
```
### Accessible Names
All interactive elements MUST have an accessible name:
```tsx
// βœ… CORRECT: Button with text content (name from content)
<button>Submit form</button>
// βœ… CORRECT: Icon button with aria-label
<button aria-label="Close dialog">
<XIcon className="h-4 w-4" />
</button>
// ❌ WRONG: Icon button without accessible name
<button>
<XIcon className="h-4 w-4" />
</button>
```
### Image Alt Text
```tsx
// βœ… CORRECT: Descriptive alt text
<Image alt="Two-story brick building with a landscaped courtyard entrance" src={src} />
// βœ… CORRECT: Decorative image (empty alt)
<Image alt="" src={decorativeBg} aria-hidden="true" />
// ❌ WRONG: Generic alt text
<Image alt="photo" src={src} />
<Image alt="image 1" src={src} />
// ❌ WRONG: Missing alt
<Image src={src} />
```
---
## Metadata & SEO
### generateMetadata Pattern
Every page with dynamic content MUST implement `generateMetadata`:
```tsx
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const { slug } = await params;
const data = await getData(slug);
if (!data) return {};
return {
title: data.title,
description: data.description,
openGraph: {
title: data.title,
description: data.description,
images: data.image ? [{ url: data.image }] : undefined,
},
alternates: {
canonical: getCanonicalUrl(slug),
},
};
}
```
### No Snippet Blocking
**NEVER add `nosnippet` to content pages.** This prevents AI feature inclusion:
```tsx
// ❌ WRONG: Blocks AI features
export const metadata = {
robots: { index: true, follow: true, nosnippet: true },
};
// βœ… CORRECT: Allow snippets (default behavior)
export const metadata = {
robots: { index: true, follow: true },
};
```
---
## Structured Data (JSON-LD)
### Implementation Pattern
Use a dedicated JSON-LD component in layout components:
```tsx
export function JsonLd({ data }: { data: Record<string, unknown> }) {
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
/>
);
}
```
### Required Schema by Page Type
| Page Type | Required Schema |
|-----------|----------------|
| Homepage | `Organization`, `WebSite` with `SearchAction` |
| Local business / Location | `LocalBusiness` with address, geo, rating |
| Blog Post | `Article` with author (`Person`), dates |
| Author Page | `Person` with credentials, `knowsAbout` |
| FAQ Section | `FAQPage` with Q&A pairs |
| All Pages | `BreadcrumbList` |
---
## Layout Stability (CLS Prevention)
```tsx
// βœ… CORRECT: Explicit dimensions prevent layout shift
<Image src={src} alt="..." width={800} height={600} />
// βœ… CORRECT: Aspect ratio container
<div className="aspect-video relative">
<Image src={src} alt="..." fill className="object-cover" />
</div>
// ❌ WRONG: No dimensions (causes layout shift)
<img src={src} alt="..." />
```
---
## Server-Side Rendering
**Critical content MUST render on the server.** Client Components should only wrap interactive UI, not content:
```tsx
// βœ… CORRECT: Content in Server Component
export default async function DetailPage({ params }: Props) {
const item = await getItem(params.slug);
return (
<main id="main-content">
<h1>{item.name}</h1>
<p>{item.description}</p>
<ContactForm itemId={item.id} /> {/* Client Component for form only */}
</main>
);
}
// ❌ WRONG: Entire page as Client Component
"use client";
export default function DetailPage() {
const [item, setItem] = useState(null);
useEffect(() => { fetchItem().then(setItem); }, []);
// Content invisible to crawlers until JS executes
}
```
# Release β€” Node / npm Partial
Version-bump and quality-check mechanics for **Node.js / npm packages** (anything with a
`package.json`). Included by `prepare-github-release` when the project is detected as a Node package.
## Detection signal
- A `package.json` exists at the repo root **and** there is no WordPress plugin header
(`*.php` file with a `Version:` plugin header). If both are present, prefer the WordPress partial.
## Version source of truth
| File | What to update |
|------|----------------|
| `package.json` | `"version": "X.Y.Z"` |
| `package-lock.json` | top-level `version` **and** the root package entry under `packages[""].version` |
Prefer the npm tooling so both files stay in sync automatically:
```bash
# Bumps package.json + package-lock.json and (by default) creates a commit + tag.
# Use --no-git-tag-version to keep the version change uncommitted so it can go in the release branch/PR.
npm version X.Y.Z --no-git-tag-version
```
If you edit `package.json` by hand instead, re-sync the lockfile without touching dependencies:
```bash
npm install --package-lock-only
```
## Quality checks (run BEFORE bumping)
Inspect `package.json` `scripts` and run whatever exists β€” do not assume script names:
```bash
npm ci # clean install when a lockfile is present (CI parity)
npm test --if-present
npm run lint --if-present
npm run type-check --if-present
npm run build --if-present
```
Do **not** proceed if any check fails.
## What the release usually produces
- **npm publish** β€” most Node packages publish to the npm registry. The publish step almost always
lives in a workflow triggered by a GitHub Release (`on: release: [created|published]`) or a tag
push β€” confirm via the workflow-analysis step in the orchestrator before assuming a bare tag is enough.
- Optional build artifacts (bundled `dist/`, types) attached to the Release.
## Notes
- `private: true` in `package.json` means the package is **not** published to npm β€” the release is
tag/GitHub-Release only. Surface this to the user.
- The npm version in `package.json` is the source of truth that `publish.yml`-style workflows read,
so it must be committed before the tag/Release is created.
# Release β€” WordPress Plugin Partial
Version-bump and quality-check mechanics for **Silver Assist WordPress plugins**. Included by
`prepare-github-release` when the project is detected as a WordPress plugin. See the
`release-management` skill for full documentation and troubleshooting.
## Detection signal
- A `*.php` file with a plugin header (`* Version: X.Y.Z`) at the repo root, usually alongside a
`composer.json` and often a `readme.txt`.
## Version source of truth
| File | What to update |
|------|----------------|
| Main plugin file | `* Version: X.Y.Z` header **and** the `VERSION` constant if defined |
| `readme.txt` | `Stable tag: X.Y.Z` (if present) |
| `composer.json` | `"version": "X.Y.Z"` (if present) |
Prefer the bundled script so every file stays in sync:
```bash
./scripts/update-version.sh X.Y.Z
# Some plugins ship the simple variant instead:
./scripts/update-version-simple.sh X.Y.Z
```
## Quality checks (run BEFORE bumping)
Do **not** proceed if any check fails:
```bash
./scripts/run-quality-checks.sh --skip-wp-setup phpcs phpstan
```
If the script is absent, fall back to the tools directly:
```bash
composer run phpcs --if-present
composer run phpstan --if-present
composer run test --if-present
```
## What the release usually produces
- **Distributable ZIP** β€” the GitHub Actions release workflow builds the plugin ZIP and attaches it
to the GitHub Release. This almost always triggers on `on: release: [created|published]`, so a bare
tag is typically **not** enough β€” confirm via the workflow-analysis step in the orchestrator.
- No npm publish; WordPress plugins are distributed as ZIP artifacts (and/or wordpress.org SVN).
## Notes
- Some plugins use `master` instead of `main` as the default branch (e.g.
`silver-assist-post-revalidate`) β€” always verify with `gh repo view --json defaultBranchRef`.
- The plugin-header version is what WordPress and the release workflow read, so it must be committed
before the tag/Release is created.
---
description: Prepare a GitHub release (version bump, changelog, tag/Release) β€” auto-detects WordPress vs Node projects and the correct tag-vs-Release flow
agent: agent
tools:
- run_in_terminal
- read_file
- replace_string_in_file
- create_file
---
# Prepare GitHub Release
Prepare a new version release and drive it through the **correct GitHub flow** for the current
project. This prompt is **project-agnostic**: it detects the ecosystem (WordPress plugin vs Node/npm
package) and analyzes the repo's GitHub Actions workflows to decide whether a **bare tag** or a full
**GitHub Release** is required.
## Prerequisites
- Reference: `.github/prompts/_partials/git-operations.md`
- Reference: `.github/prompts/_partials/release-wordpress.md` (WordPress projects)
- Reference: `.github/prompts/_partials/release-node.md` (Node/npm projects)
- `gh` CLI authenticated. Releases are a GitHub concept β€” if the `origin` remote is **not** GitHub
(e.g. Bitbucket), stop and tell the user this flow does not apply.
## Inputs
Ask the user:
1. **Version type** β€” `patch`, `minor`, or `major`? (default: patch). Suggest one from the
`[Unreleased]` changelog content: new features β†’ `minor`, fixes only β†’ `patch`, breaking β†’ `major`.
2. **Changelog entry** β€” reuse the existing `[Unreleased]` section if present, otherwise offer to
generate one from `git log` since the last tag.
## Steps
### 1. Detect host and project type
```bash
git remote get-url origin # must be github.com β€” else stop
git fetch --tags --quiet
git tag --sort=-v:refname | head -1 # latest tag (current released version)
```
Detect the **project type** and follow the matching partial:
| Signal | Project type | Use partial |
|--------|--------------|-------------|
| `*.php` with a `* Version:` plugin header | WordPress plugin | `release-wordpress.md` |
| `package.json` (no WP header) | Node / npm | `release-node.md` |
| neither | Generic | inline fallback: bump a `VERSION` file / changelog only |
### 2. Determine the new version
- Read the **current** version from the source of truth named in the matching partial (not just the
git tag β€” they can drift).
- Apply the bump type to compute `X.Y.Z`.
- **NEVER reuse an existing tag** β€” tags are immutable. If `vX.Y.Z` already exists, bump again.
### 3. Run quality checks (per partial)
Run the ecosystem's checks from the matching partial (WordPress: phpcs/phpstan; Node: `npm test` +
lint/type-check/build). **Do not proceed if any check fails.**
### 4. Bump the version (per partial)
Update every version file listed in the matching partial (WordPress: plugin header / `readme.txt` /
`composer.json`; Node: `package.json` + `package-lock.json`).
### 5. Update CHANGELOG.md
Promote the `[Unreleased]` section to the new version, keeping a Keep-a-Changelog structure:
```markdown
## [X.Y.Z] - YYYY-MM-DD
### Added/Changed/Fixed
- ...
```
If there is no `[Unreleased]` section, add a new `## [X.Y.Z]` block at the top with the changes.
### 6. Create release branch, commit, and PR
```bash
BASE_BRANCH=$(node -e "try{const c=require('./.agents-toolkit.json');console.log(c.pr?.targetBranch||c.git?.defaultBranch||'main')}catch{console.log('main')}")
git checkout -b release/vX.Y.Z
git add -A
git commit -m "chore: bump version to X.Y.Z"
git push -u origin release/vX.Y.Z
gh pr create --base "$BASE_BRANCH" --title "Release vX.Y.Z" --body "## Changes
- changelog entry" | cat
```
### 7. Analyze workflows β†’ decide tag-only vs GitHub Release
**This is the critical step.** Read the repo's workflows and tell the user exactly what to do after
the release PR merges:
```bash
ls .github/workflows/ 2>/dev/null
# inspect the `on:` triggers and publish/build steps of each workflow
```
Decide from the triggers:
| Workflow trigger | Post-merge action | Why |
|------------------|-------------------|-----|
| `on: release: [created\|published]` | **Create a GitHub Release** (`gh release create vX.Y.Z`) | A bare tag does **not** fire `release` workflows β€” publishing/build only runs on the Release event |
| `on: push: tags: ['v*']` | **Push the tag** (`git push origin vX.Y.Z`) β€” Release optional | The tag push alone triggers the workflow |
| neither / no workflow | **Push the tag** for history; optionally `gh release create` for visibility | No automation depends on it |
Also scan the workflow **steps** and report what the release produces (e.g. `npm publish`, plugin
ZIP artifact, Docker image) so the user knows what will happen.
### 8. Post-merge instructions (tailored to step 7)
Give the exact commands for the detected flow, e.g.:
```bash
git checkout "$BASE_BRANCH" && git pull
git tag vX.Y.Z
git push origin vX.Y.Z
# If a `release:`-triggered workflow exists, ALSO create the Release so it fires:
gh release create vX.Y.Z --generate-notes
```
## Important
- **NEVER reuse an existing tag** β€” tags are immutable. If a tag exists, bump to the next version.
- The `release-management` skill has full documentation for WordPress plugins β€” use it for troubleshooting.
- Some projects use `master` instead of `main` β€” always verify the default branch with
`gh repo view --json defaultBranchRef | cat`.
- A bare tag and a GitHub Release are **not** interchangeable: many publish/build workflows only run
on the `release` event. Always complete step 7 before telling the user the release is done.
---
name: nextjs-caching
description: Caching strategy for Next.js (App Router) frontends consuming the CCDS API and headless WordPress. Use when asked about caching, ISR, "revalidate", "stale data", "page not cached", "private / no-store header", POST requests not caching, CloudFront/CDN invalidation, `next: { revalidate, tags }`, or when a page unexpectedly renders dynamically.
---
# Next.js Caching Skill
Canonical caching strategy for Silver Side Next.js (App Router) frontends. Use it when touching data
fetching, route segment configs, the asset/page proxy, image config, or the on-demand revalidate
route β€” and when diagnosing "why isn't this page cached / why is it serving stale".
> Companion: `caching.instructions.md` (the short, auto-applied rules). This skill is the deep
> reference and decision guide. If a project keeps a `docs/CACHING.md`, that is its project-specific
> extension (endpoints, CloudFront IDs, per-route tiers) and is never overwritten by the toolkit.
---
## Core principle
**Cache by intent (read vs. mutation), never by the HTTP method.** Some reads must use `POST` because
they take a body (e.g. CCDS `geo-search`). They still have to cache like a `GET`. Mutations must never
cache. Getting this wrong silently turns a static page into a per-request dynamic render.
---
## The caching layers (independent)
Fixing one does **not** fix the others.
| # | Layer | Where | Purpose |
|---|-------|-------|---------|
| 1 | ISR / page cache | `export const revalidate` per route | Time-based regeneration of rendered pages |
| 2 | Data-fetch cache | `next: { revalidate, tags }` on each `fetch` | Cross-request caching of CCDS/WP responses |
| 3 | Request dedup | `React.cache()` around client fns | Collapse duplicate calls within one render |
| 4 | Asset proxy | `src/app/assets/[...path]/route.ts` | Long-lived `Cache-Control` on WP images/CSS/fonts |
| 5 | Edge / CDN | `src/proxy.ts` + CloudFront | `s-maxage` at the edge + on-demand invalidation |
**Layer 2 is the most commonly broken.** A page can be ISR-enabled (layer 1) yet still hit origin on
every request if its data fetches (layer 2) are not cached β€” and an uncached fetch also forces the
whole route into **dynamic rendering**, which emits `cache-control: private, no-cache, no-store`.
---
## How Next.js 16 actually decides (the rule behind the rule)
From `next/dist/server/lib/patch-fetch.js`:
- `fetch` is **not cached by default**. A non-`GET` method is "uncacheable" **only when no explicit
cache config is provided**. Concretely, a POST is auto-no-cached only when it has no `next`/`cache`
options *and* the segment `revalidate` is `0`.
- Therefore **a `POST` IS cached cross-request when you pass an explicit `next: { revalidate }`** (and
the segment isn't `revalidate: 0`). This is why the WordPress GraphQL POST caches fine.
- The **request body is part of the cache key** (`incremental-cache` β†’ `generateCacheKey` hashes
`init.body`), so different filter bodies cache independently and never collide.
**Implication:** you do **not** need `unstable_cache` or a proxy header override to cache POST reads.
Just send `next: { revalidate, tags }`.
---
## Canonical API client (read vs. mutation)
```ts
interface FetchOptions {
endpoint: string;
method?: "GET" | "POST" | "PUT" | "DELETE";
body?: object | null;
revalidateTag?: string;
revalidate?: number;
/** Writes are never cached. Reads (default) cache regardless of method. */
mutation?: boolean;
}
export async function fetchData<T>({
endpoint,
method = "GET",
body = null,
revalidateTag,
revalidate = 86400, // 24h time-based fallback; pair with tags for surgical invalidation
mutation = false,
}: FetchOptions): Promise<T | null> {
const isMutation = mutation || method === "PUT" || method === "DELETE";
const requestOptions: RequestInit = {
method,
// Reads (GET or POST) cache cross-request via `next`; mutations opt out.
...(isMutation
? { cache: "no-store" as RequestCache }
: { next: { revalidate, tags: revalidateTag ? [revalidateTag] : [] } }),
};
if ((method === "POST" || method === "PUT") && body) {
requestOptions.body = JSON.stringify(body);
}
// ...fetch + error handling...
}
```
Mark every write explicitly:
```ts
await fetchData({ endpoint: "community/submit-review", method: "POST", body, mutation: true });
```
WordPress GraphQL client (POST read β€” must carry `next`):
```ts
await fetch(WP_API_URL, {
method: "POST",
body: JSON.stringify({ query, variables }),
next: { revalidate, ...(tags && { tags }) }, // a POST IS cacheable WITH next options
});
```
---
## ISR revalidate tiers
| Route type | `export const revalidate` |
|------------|---------------------------|
| Listing / index | `2592000` (30d) β€” webhook handles freshness |
| Detail / city / community (`[slug]`) | `86400` (24h) |
| State / advisor landing | `604800` (7d) |
| WP catch-all (`[[...uri]]`) | `604800` (7d) |
- Every cacheable route exports `revalidate`. Do **not** add it to form/personalized routes.
- Prefer `export const revalidate` over `dynamic = "force-static"`: a failed ISR revalidation then
preserves the last good cache instead of overwriting it with a broken page. Pair with a client that
**throws on 5xx at runtime** (so ISR keeps the previous version) but returns an error during the
build phase (so `generateStaticParams` can skip a bad entry without failing the build).
---
## On-demand revalidation (dual invalidation)
`/api/revalidate` must invalidate **both** layers in one request, or the CDN serves stale until its
own TTL expires:
```ts
revalidatePath(path, "page"); // or "layout"
revalidateTag(tag); // granular per-state / per-city tags
if (invalidateCDN) await invalidateCloudFrontPaths([path]);
```
---
## Anti-patterns
- ❌ `next: method === "GET" ? {...} : { revalidate: 0 }` β€” leaves POST reads uncached β†’ the route
renders dynamically (`private, no-store`). This is the exact regression that broke city pages.
- ❌ Bare `fetch(url, { method: "POST", body })` for a read β€” refetched from origin every render.
- ❌ `next: { tags }` with no `revalidate` β€” holds stale data or doesn't cache at runtime.
- ❌ Caching a mutation (`submit`, lead, `PUT`/`DELETE`/`PATCH`).
- ❌ Treating `React.cache()` as cross-request caching (it's request-scoped dedup only).
- ❌ `dynamic = "force-static"` on a page whose revalidation can fail (caches a broken page).
- ❌ Mixing `cacheComponents: true` (`"use cache"`) with route-segment `export const revalidate` β€”
that is a separate, deliberate migration; do not introduce it ad hoc.
---
## Diagnosing "page is not cached"
1. **Check the response header.** `cache-control: private, no-cache, no-store` β‡’ the route is rendering
dynamically. Something opted it into dynamic rendering.
2. **Find the dynamic trigger.** Usually an uncached fetch (a POST read without `next`, or
`revalidate: 0`), or a request-time API (`cookies()`, `headers()`, `searchParams`).
3. **Fix layer 2 first.** Give read fetches `next: { revalidate, tags }`; mark mutations `no-store`.
4. **Confirm the route exports `revalidate`.**
5. **Reconcile CDN vs. ISR TTLs.** If ISR is 24h but CDN `s-maxage` is shorter, an ISR revalidation
should trigger a CloudFront invalidation for that path.
---
## Verification
```bash
# A POST-read page (e.g. a city) must be publicly cacheable:
curl -sI https://<host>/<care-type>/<state>/<city> | grep -i cache-control
# expect: cache-control: public, ... (NOT private/no-store)
```
In the build output, `●` (or "Static"/"ISR") means prerendered; `Ζ’` ("Dynamic") means it renders per
request β€” a POST-read page should be the former. `next.config` `logging.fetches.fullUrl: true` shows
per-fetch cache decisions in dev.
+5
-3

@@ -54,3 +54,3 @@ #!/usr/bin/env node

instructions: {
react: ['css-styling', 'react-components', 'server-actions', 'tests', 'typescript'],
react: ['caching', 'css-styling', 'react-components', 'seo-ai-optimization', 'server-actions', 'tests', 'typescript'],
wordpress: ['php-standards', 'wordpress-plugin-architecture', 'testing-standards'],

@@ -66,3 +66,3 @@ universal: ['documentation-language', 'github-workflow'],

'create-github-pr', 'finalize-github-pr',
'review-code', 'fix-issues', 'add-tests', 'prepare-release',
'review-code', 'fix-issues', 'add-tests', 'prepare-github-release',
],

@@ -73,2 +73,4 @@ jira: ['analyze-ticket', 'work-ticket', 'create-pr', 'finalize-pr'],

partials: {
react: ['release-node'],
wordpress: ['release-wordpress'],
jira: ['jira-integration'],

@@ -79,3 +81,3 @@ github: ['github-integration'],

skills: {
react: ['component-architecture', 'testing-patterns'],
react: ['component-architecture', 'nextjs-caching', 'testing-patterns'],
wordpress: ['create-component', 'plugin-creation', 'quality-checks', 'testing'],

@@ -82,0 +84,0 @@ universal: ['domain-driven-design', 'release-management'],

@@ -38,3 +38,3 @@ # PolyForm Noncommercial License 1.0.0

> Required Notice: Copyright SilverAssist (https://silverassist.com)
> Required Notice: Copyright SilverAssist (<https://silverassist.com>)

@@ -136,2 +136,2 @@ ## Changes and New Works License

Required Notice: Copyright 2026 SilverAssist (https://silverassist.com)
Required Notice: Copyright 2026 SilverAssist (<https://silverassist.com>)
{
"name": "@silverassist/agents-toolkit",
"version": "2.4.0",
"version": "2.5.0",
"description": "Reusable AI agent prompts for development workflows with Jira integration β€” supports GitHub Copilot, Claude Code, and Codex",

@@ -5,0 +5,0 @@ "author": "Santiago Ramirez",

@@ -234,2 +234,3 @@ # @silverassist/agents-toolkit

| `finalize-github-pr` | Finalize and merge PR (GitHub) | `{issue-number}` | GitHub |
| `prepare-github-release` | Prepare a GitHub release (auto-detects WordPress vs Node, tag vs Release) | β€” | GitHub |

@@ -420,2 +421,4 @@ ### Utility

| `pr-template.md` | Pull request templates (GitHub Issues + Jira) |
| `release-node.md` | Node/npm release bump & quality checks (used by `prepare-github-release`) |
| `release-wordpress.md` | WordPress plugin release bump & quality checks (used by `prepare-github-release`) |

@@ -433,2 +436,4 @@ ## Instructions

| `css-styling.instructions.md` | `*.css, *.tsx` | Tailwind CSS & shadcn/ui standards |
| `caching.instructions.md` | `next.config.*, src/proxy.ts, src/lib/**, **/route.ts, **/page.tsx` | Next.js caching: read-vs-mutation fetch caching, ISR tiers, CDN invalidation |
| `seo-ai-optimization.instructions.md` | `*.tsx` | Semantic HTML, accessibility tree, metadata, JSON-LD & E-E-A-T for AI Search |

@@ -443,2 +448,3 @@ ## Skills

| `domain-driven-design` | DDD principles, domain organization, barrel exports |
| `nextjs-caching` | Next.js caching strategy: read-vs-mutation fetch, ISR tiers, CDN invalidation, diagnosing dynamic-render leaks |
| `testing-patterns` | Jest + RTL patterns for Next.js 15 and Server Actions |

@@ -445,0 +451,0 @@

@@ -6,3 +6,3 @@ /**

export const VERSION = "2.4.0";
export const VERSION = "2.5.0";

@@ -18,4 +18,4 @@ export const PROMPTS = {

"finalize-pr",
"prepare-github-release",
"prepare-pr",
"prepare-release",
"work-github-issue",

@@ -41,2 +41,4 @@ "work-ticket",

"pr-template",
"release-node",
"release-wordpress",
"validations",

@@ -46,2 +48,3 @@ ];

export const INSTRUCTIONS = [
"caching",
"css-styling",

@@ -52,2 +55,3 @@ "documentation-language",

"react-components",
"seo-ai-optimization",
"server-actions",

@@ -65,2 +69,3 @@ "testing-standards",

"domain-driven-design",
"nextjs-caching",
"plugin-creation",

@@ -67,0 +72,0 @@ "quality-checks",

@@ -16,7 +16,9 @@ # Copilot Coding Agent Instructions

[Instructions]|root:.github/instructions
|css-styling.instructions.md β†’ CSS/Tailwind patterns, cn() utility, responsive design
|react-components.instructions.md β†’ Component structure, exports, props, early returns
|server-actions.instructions.md β†’ Server action patterns, validation, error handling
|tests.instructions.md β†’ Test structure, mocking, assertions
|typescript.instructions.md β†’ Type safety, destructuring, JSDoc
|caching.instructions.md β†’ Next.js caching: read-vs-mutation fetch, ISR tiers, CDN invalidation
|css-styling.instructions.md β†’ CSS/Tailwind patterns, cn() utility, responsive design
|react-components.instructions.md β†’ Component structure, exports, props, early returns
|seo-ai-optimization.instructions.md β†’ Semantic HTML, a11y tree, metadata, JSON-LD for AI Search
|server-actions.instructions.md β†’ Server action patterns, validation, error handling
|tests.instructions.md β†’ Test structure, mocking, assertions
|typescript.instructions.md β†’ Type safety, destructuring, JSDoc

@@ -137,2 +139,26 @@ [Prompts]|root:.github/prompts

## πŸ—„οΈ Caching Rules (CRITICAL)
| Rule | Requirement |
|------|-------------|
| **Cache by intent** | Cache reads, never mutations β€” decide on intent, NOT the HTTP method |
| **POST reads** | A `POST` read (e.g. filter/`geo-search`) MUST set `next: { revalidate, tags }` to cache |
| **Mutations** | `submit`/lead/`PUT`/`DELETE` β†’ `cache: "no-store"` (flag with `mutation: true`) |
| **ISR** | Every cacheable route exports `revalidate`; never on form/personalized routes |
| **Invalidation** | `/api/revalidate` calls `revalidateTag`/`revalidatePath` AND the CDN invalidation |
```ts
// ❌ WRONG: leaves POST reads uncached β†’ route turns dynamic (private, no-store)
next: method === "GET" ? { revalidate, tags } : { revalidate: 0 }
// βœ… CORRECT: reads cache regardless of method; mutations opt out
const isMutation = mutation || method === "PUT" || method === "DELETE";
...(isMutation ? { cache: "no-store" } : { next: { revalidate, tags } })
```
πŸ“„ **Full details:** `.github/instructions/caching.instructions.md`
πŸ“„ **Project-specific context (if present):** `docs/CACHING.md`
---
## πŸ“ Git Conventions

@@ -195,4 +221,6 @@

| Creating server actions | `server-actions.instructions.md` |
| Data fetching / `next.config` / routes / ISR / revalidate | `caching.instructions.md` |
| Pages, metadata, JSON-LD, accessibility/SEO | `seo-ai-optimization.instructions.md` |
| Writing tests | `tests.instructions.md` |
| TypeScript questions | `typescript.instructions.md` |
| **Before pushing/PR** | `tests.instructions.md` + run quality checks |

@@ -88,1 +88,2 @@ # Validations Partial

- [ ] JSDoc comments on new functions
- [ ] If data fetching / routes / `next.config` changed: caching follows `caching.instructions.md` (reads cache regardless of method, mutations `no-store`, cacheable routes export `revalidate`)

@@ -58,2 +58,10 @@ ---

### Caching Impact (Next.js)
If the feature touches data fetching, routes, or `next.config`, state the caching plan:
- Reads vs. mutations (mutations never cached; reads cache regardless of GET/POST)
- `revalidate` tier + cache `tags` for any new fetches
- Whether new routes export `revalidate`, and any on-demand invalidation (Next.js + CDN) needed
See `.github/instructions/caching.instructions.md`.
## Risk Assessment

@@ -60,0 +68,0 @@

@@ -59,2 +59,14 @@ ---

### 5. Caching & Data Fetching (Next.js)
> See `.github/instructions/caching.instructions.md`. Caching is decided by **read-vs-mutation**, not the HTTP method.
- [ ] Reads cache regardless of method β€” a `POST` read (e.g. `geo-search`) sets `next: { revalidate, tags }` (NOT `revalidate: 0`)
- [ ] No `next: method === "GET" ? {...} : { revalidate: 0 }` gating (leaves POST reads uncached β†’ route turns dynamic, `private, no-store`)
- [ ] Mutations (`submit`/lead/`PUT`/`DELETE`) use `cache: "no-store"` and are never cached
- [ ] `tags` always paired with a `revalidate` duration (no bare `next: { tags }`)
- [ ] Cacheable routes export `revalidate`; not added to form/personalized routes
- [ ] `React.cache()` not used as a substitute for cross-request caching
- [ ] On-demand revalidation invalidates both Next.js (`revalidateTag`/`revalidatePath`) and the CDN
## Output

@@ -61,0 +73,0 @@

---
description: Prepare a new release for a Silver Assist WordPress plugin (version bump, changelog, tag, PR)
agent: agent
tools:
- run_in_terminal
- read_file
- replace_string_in_file
- create_file
---
# Prepare Release
Prepare a new version release for the current Silver Assist plugin.
## Inputs
Ask the user:
1. **Version type** β€” Is this a `patch`, `minor`, or `major` release? (default: patch)
2. **Changelog entry** β€” What changed? (or offer to generate from recent commits)
## Steps
1. **Detect plugin** β€” Find the plugin root, main plugin file, and current version.
2. **Determine new version** β€” Based on the version type, calculate the next semantic version.
3. **Run quality checks** β€” Execute `./scripts/run-quality-checks.sh --skip-wp-setup phpcs phpstan` to verify the code is clean before release. Do NOT proceed if checks fail.
4. **Update version** β€” Run the version update script:
```bash
./scripts/update-version.sh X.Y.Z
```
Or if the plugin uses the simple variant:
```bash
./scripts/update-version-simple.sh X.Y.Z
```
This updates the version in the main plugin file, `readme.txt` (if present), and `composer.json`.
5. **Update CHANGELOG.md** β€” Add a new entry at the top following this format:
```markdown
## [X.Y.Z] - YYYY-MM-DD
### Added/Changed/Fixed
- Description of change
```
6. **Create release branch and commit** β€” Following the branch naming convention:
```bash
git checkout -b release/vX.Y.Z
git add -A
git commit -m "chore: bump version to X.Y.Z"
```
7. **Push and create PR** β€” Push the branch and create a PR:
```bash
git push origin release/vX.Y.Z
gh pr create --title "Release vX.Y.Z" --body "## Changes\n\n- changelog entry" | cat
```
8. **Post-merge instructions** β€” Remind the user:
- After merging the PR, create the tag from the main/master branch:
```bash
git checkout main && git pull
git tag vX.Y.Z
git push origin vX.Y.Z
```
- The GitHub Actions release workflow will automatically build the ZIP and create the GitHub Release.
## Important
- **NEVER reuse an existing tag** β€” tags are immutable. If a tag exists, bump to the next version.
- The release-management skill has full documentation β€” use it for troubleshooting.
- Some plugins use `master` instead of `main` (e.g., silver-assist-post-revalidate).
- Always verify the default branch with `git branch --show-current` or `gh repo view --json defaultBranchRef | cat`.