The problem
What problem does Assistant knowledge sync solve?
A site with an AI chat assistant has two copies of everything it says: the page, and what the assistant was told about the page. The first copy changes every week. The second changes when someone remembers. A month later the assistant links to a route that moved, describes a program the way it was before the rewrite, and quotes a price the site never published.
A longer prompt fixes it for one deploy. The facts were retyped, so they drift again with the next edit, and nothing shows that the model actually uses them when a visitor asks in their own words.
This skill gives a coding agent the update the way we run it on our own assistant: an audit of the gap between the pages and the assistant, page copy moved byte for byte into one data module that the page and the assistant both import, new pages added to the knowledge pack and the searchable corpus, a site map built from the sitemap's own sources, an explicit "never quote" line for every number the site has not published, and unit tests and live probes asked in a visitor's words that check routes and numbers.
The tool
What does the skill do?
This skill adds no module to your app: your agent runs it as a tool. What Assistant knowledge sync does:
- 1. A gap audit between pages and assistant
- 2. Page copy moved into one data module the page and the assistant both read
- 3. The knowledge pack and searchable corpus
- 4. A site map derived from the sitemap source
- 5. Routing and never-quote guardrails
- 6. Unit tests and regex probes on routes and numbers
Provenance
Where do the rules come from?
This skill was written by the engineer who has shipped this work. The earlier implementation it was audited against was the assistant of a marketing site, answering on more than one channel from one knowledge module. The procedure holds the properties an assistant update has to hold: page copy moved byte-for-byte so the page renders identically, a site map that gains a route whenever the sitemap does, tool descriptions that name every document kind in the corpus, the page's version shown where an external index lags it, and an explicit "never quote" line for every number the site has not published. Unit tests, a render check and regex probes verify each one; `references/provenance.md` has the record.
The engineering ledger for the person editing this skill. It separates what the audit of the earlier implementation changed and how the procedure now verifies it, what was kept deliberately and why it is safe, and what was designed in the skill and has never run. Read it before simplifying anything; add an entry for anything you change.
The ledger separates what the audit changed, what was kept on purpose, and what has not run in production yet.
Fixed
- Page copy unreachable by the assistant The program page kept its promises, roles, steps, badges, permissions and FAQ as constants inside page and component files. The assistant had no way to read them, so it had nothing to say about the program. Now: the single-source refactor in single-source.md; the render check in verification.md proves the page unchanged, and a unit test proves the pack holds the moved copy.
- Hand-written site map drifted from the sitemap The assistant's site map was a hand-kept list. The sitemap gained the catalog, the program, the articles and the legal pages; the assistant's list gained none of them. Now:
siteMapLinesbuilt from the sitemap's arrays in pack-and-corpus.md, and the sitemap comparison table in gap-audit.md. - Live index older than the synced content The fetched index README listed an older version of an entry than its synced changelog and its page. Now:
joinedRows, which prefers the page's version, in pack-and-corpus.md. - Catalog pages not in the corpus Entries were listed in the pack by name only; their requirements, rules and details were on the page but unreachable. Now: the
catalogdocument kind andcatalogDocument, with a unit test that finds an entry by what it does. - Tool descriptions listed the old kinds The search tool named only the help center, the blog and case studies, so the model had no reason to search for catalog or program pages. Now: the tool description step in guardrails.md, and hard rule 3.
Show 4 moreShow fewer
- Links pointed off-site The web prompt linked catalog entries to their source repositories although each had a page on the site. Now: the link-preference guardrail, with a probe that expects the site path.
- Unpublished numbers Payout shares are not published. Without a line saying so, the model is free to estimate one. Now: the "never quote" guardrail, with a probe that rejects any percentage.
- Wrong kind, wrong offer The prompt said every catalog entry was something to buy and build. Some entries had nothing to sell, and one targeted a different platform. Now: the kind-distinction guardrail and
catalogOffer(), which states the offer exactly as the page does, with a unit test on an entry that is not for sale. - Proper nouns lowercased A pack line ran
.toLowerCase()over whole sentences to fit them mid-line, which lowercased the company name in the prompt. Found while writing this skill. Now: the "print the pack and read it" step in verification.md.
Non-negotiables
What are the 5 rules the module never breaks?
Every module built from this skill holds these, whoever builds it. The same list is in the skill's README and SKILL.md, so the agent reads it before it writes a line.
Never retype page copy into the prompt.
Import it from the module the page renders. A retyped copy is correct for one deploy; a unit test on the pack proves the imported line is there.
Never rewrite the copy while moving it.
A single-source refactor is byte-for-byte and the page renders identically, which the render check verifies. Wording changes are a separate commit and a separate decision.
Never add a document kind without updating the tool descriptions and link rules.
The model decides to search from the tool's description, not from what the corpus holds; a unit test finds the new kind and a probe expects its site path.
Never ship without a probe per new fact and one per guardrail.
Unit tests prove the pack contains the line; only a live probe proves the model uses it. With no model reachable, probes are reported as not run, never as passing.
Never let the pack grow unbounded.
Indexes and summaries go in the pack; full bodies stay behind the read tool. The pack's size is measured before and after.
Fit
When should you use it, and when not?
Use it for
- A page, section, partner program, product, catalog entry or route was added or rewritten.
- The sitemap changed and the assistant's site map did not.
- The assistant gives stale versions, wrong links, invented numbers, or routes people to the wrong form.
- After a content sync (a catalog pulled from another repository) the assistant still lists the old one.
Not for
- Building the assistant, its chat widget or its streaming transportInsteadai-sdk for chat on the site, or slack-ai-bot for a Slack bot
- Embeddings, a vector store or hosted RAGInsteadA RAG stack; this skill keeps an in-repo pack and a keyword corpus
- Writing or editing the page copyInsteadThe host's content workflow; this skill moves copy and never rewrites it
- Changing the model, provider or failoverInsteadThe host's AI configuration
- A help center or blog the assistant should readInsteadhelp-center-markdown or blog-markdown to build it, then this skill to teach the assistant
Install
How do I install it?
One command. The skills.sh CLI installs the skill into every skills-compatible agent it finds.
$ npx skills add timerise-ai/assistant-knowledge-syncClaude Code
Invoke with /assistant-knowledge-sync
Codex CLI
Invoke with $assistant-knowledge-sync
Gemini CLI
Invoke with /skills
Name the agents instead with -a, for example npx skills add timerise-ai/assistant-knowledge-sync -a claude-code -a codex. Or clone the repository into your agent's skills folder. Nothing in it is agent-specific.
What is inside the repository (11 entries)
SKILL.mdEntry point: architecture diagram, critical facts, hard rules, unattended runs, quick start, and the reference directoryreferences/adaptation.mdThe seam contract: what the host must have, the seam table, the knowledge interface, the rename table, integration points, order of work, the non-negotiablesreferences/surfaces.mdThe seven places assistant knowledge lives and the read-only host probereferences/gap-audit.mdWhat changed, the sitemap comparison, the fact list, the baseline probesreferences/single-source.mdMoving page copy into a shared data module without changing a wordreferences/pack-and-corpus.mdDocument kinds, catalog and page documents, pack sections, the site map, the live index join, size budgetsreferences/guardrails.mdRouting, links, unpublished facts, kind distinctions, tool descriptions, every channelreferences/verification.mdUnit tests, the pack dump, the render check, live probes, a run with no model reachablereferences/provenance.mdThe engineering ledger: what the audit of the earlier implementation changed and how it is verified, what was kept on purpose, what is new in the skillevals/The prompts an operator types after installing (prompts.md) and one file per agent eval: the skill installed into an empty Next.js app, one prompt, no help, then type-checked, built and tested.github/workflows/agent-eval.ymlThe caller of the index's reusable eval workflow, run on every published release
Recent releases
- v0.1.0October 8, 2026
First release: a procedure for teaching a Next.js site's AI chat assistant a new page, program or catalog from the repository's own content, verified by unit tests, a render check and regex probes.
After installing
What do I tell my agent?
Say what you need in your own words; the skill supplies the how. These are starting points, and the ones we tested say how it went.
Our site has a chat assistant; its code is below, save each file at its path first. We just shipped a partners page and the assistant knows nothing about it: people who ask about partnering get sent to sales, and yesterday it told someone they would earn 20%. Teach it about the partner program. // lib/ai/knowledge.ts export type KnowledgeKind = "help" | "page"; export interface KnowledgeDocument { kind: KnowledgeKind; title: string; url: string; summary: string; tags: string[]; content: string; } const DOCS: KnowledgeDocument[] = [ { kind: "help", title: "Booking widget", url: "/help/booking-widget", summary: "Embed a booking calendar on your site.", tags: ["widget", "calendar", "embed"], content: "Paste the script tag before the closing body tag. The widget reads your services and opening hours.", }, { kind: "page", title: "Pricing", url: "/pricing", summary: "Plans and prices.", tags: ["price", "plan", "cost"], content: "Starter: 29 EUR a month. Pro: 79 EUR a month.", }, ]; export function searchKnowledge(locale: string, query: string, limit: number): KnowledgeDocument[] { const tokens = query.toLowerCase().split(/\W+/).filter(Boolean); return DOCS.map((doc) => { const text = `${doc.title} ${doc.tags.join(" ")} ${doc.summary} ${doc.content}`.toLowerCase(); return { doc, score: tokens.filter((t) => text.includes(t)).length }; }) .filter((r) => r.score > 0) .sort((a, b) => b.score - a.score) .slice(0, limit) .map((r) => r.doc); } export function readKnowledgeDocument(locale: string, url: string): KnowledgeDocument | undefined { return DOCS.find((doc) => doc.url === url); } export function getKnowledgePack(locale: string): string { return ["#### SITE MAP", "- Pricing: /pricing", "- Help: /help/booking-widget", "- Talk to sales: /contact"].join("\n"); } // lib/ai/prompts.ts import { getKnowledgePack } from "./knowledge"; export const searchDescription = "Full-text search across the help center and the pricing page."; export function buildSystemPrompt(locale: string): string { return [ "You are Ada, the assistant on our website. Answer questions about our booking software.", "Only discuss booking, pricing and help articles. For anything else, send people to /contact.", "Link pages with relative links.", getKnowledgePack(locale), ].join("\n\n"); } // app/partners/page.tsx const ROLES = [ { name: "Referrer", body: "Introduce a client. We do the setup." }, { name: "Implementer", body: "Set up the booking widget for your client. We review it before it goes live." }, ]; const FAQ = [ { q: "Who can join?", a: "Agencies and freelancers who set up booking for their clients." }, { q: "How are partners paid?", a: "A share of the first year's subscription for every client you bring. The shares are published when the first round opens." }, ]; export default function PartnersPage() { return ( <main> <h1>Partner program</h1> <p>Bring clients to us and get paid for every one.</p> <h2>Roles</h2> <ul> {ROLES.map((role) => ( <li key={role.name}> <strong>{role.name}</strong>: {role.body} </li> ))} </ul> <h2>FAQ</h2> {FAQ.map((item) => ( <details key={item.q}> <summary>{item.q}</summary> <p>{item.a}</p> </details> ))} <p> <a href="/partners/apply">Apply to the program</a> </p> </main> ); }
Our chatbot keeps telling visitors the Pro plan costs 49 EUR, but the pricing page says 79. Fix it so the two cannot drift apart again.
No data storeLast month we renamed /pricing to /plans and added a catalog with a page per integration. Check whether our site assistant still sends people to the right places and tell me what you would change; do not edit anything yet.
No data store
Tested
How does it do in each agent?
We install the skill into an empty Next.js app, give the agent one of the prompts above and no further help, then type-check, build and run the tests it left behind. Nothing is fixed by hand before the checks, and a failing run is published like a passing one. The procedure and every result are public, and the first prompt runs again before each release.
- Built, checks pass
Gemini CLI0.63.0
gemini-3.8-flash
Our site has a chat assistant; its code is below, save each file at its path first. We just shipped a partners page and the assistant knows nothing about it: people who ask about partnering get sent to sales, and yesterday it told someone they would earn 20%. Teach it about the partner program. // lib/ai/knowledge.ts export type KnowledgeKind = "help" | "page"; export interface KnowledgeDocument { kind: KnowledgeKind; title: string; url: string; summary: string; tags: string[]; content: string; } const DOCS: KnowledgeDocument[] = [ { kind: "help", title: "Booking widget", url: "/help/booking-widget", summary: "Embed a booking calendar on your site.", tags: ["widget", "calendar", "embed"], content: "Paste the script tag before the closing body tag. The widget reads your services and opening hours.", }, { kind: "page", title: "Pricing", url: "/pricing", summary: "Plans and prices.", tags: ["price", "plan", "cost"], content: "Starter: 29 EUR a month. Pro: 79 EUR a month.", }, ]; export function searchKnowledge(locale: string, query: string, limit: number): KnowledgeDocument[] { const tokens = query.toLowerCase().split(/\W+/).filter(Boolean); return DOCS.map((doc) => { const text = `${doc.title} ${doc.tags.join(" ")} ${doc.summary} ${doc.content}`.toLowerCase(); return { doc, score: tokens.filter((t) => text.includes(t)).length }; }) .filter((r) => r.score > 0) .sort((a, b) => b.score - a.score) .slice(0, limit) .map((r) => r.doc); } export function readKnowledgeDocument(locale: string, url: string): KnowledgeDocument | undefined { return DOCS.find((doc) => doc.url === url); } export function getKnowledgePack(locale: string): string { return ["#### SITE MAP", "- Pricing: /pricing", "- Help: /help/booking-widget", "- Talk to sales: /contact"].join("\n"); } // lib/ai/prompts.ts import { getKnowledgePack } from "./knowledge"; export const searchDescription = "Full-text search across the help center and the pricing page."; export function buildSystemPrompt(locale: string): string { return [ "You are Ada, the assistant on our website. Answer questions about our booking software.", "Only discuss booking, pricing and help articles. For anything else, send people to /contact.", "Link pages with relative links.", getKnowledgePack(locale), ].join("\n\n"); } // app/partners/page.tsx const ROLES = [ { name: "Referrer", body: "Introduce a client. We do the setup." }, { name: "Implementer", body: "Set up the booking widget for your client. We review it before it goes live." }, ]; const FAQ = [ { q: "Who can join?", a: "Agencies and freelancers who set up booking for their clients." }, { q: "How are partners paid?", a: "A share of the first year's subscription for every client you bring. The shares are published when the first round opens." }, ]; export default function PartnersPage() { return ( <main> <h1>Partner program</h1> <p>Bring clients to us and get paid for every one.</p> <h2>Roles</h2> <ul> {ROLES.map((role) => ( <li key={role.name}> <strong>{role.name}</strong>: {role.body} </li> ))} </ul> <h2>FAQ</h2> {FAQ.map((item) => ( <details key={item.q}> <summary>{item.q}</summary> <p>{item.a}</p> </details> ))} <p> <a href="/partners/apply">Apply to the program</a> </p> </main> ); }
Typecheck: passBuild: passTests: pass- Time
- 7 min
- Changed
- 13 files, +1,502 lines
- Stack
- No data store
- Skill
- v0.1.0
- Run
- Oct 8, 2026
- Built, checks pass
Codex CLIcodex-cli 0.161.0
gpt-6.1-sol
Our site has a chat assistant; its code is below, save each file at its path first. We just shipped a partners page and the assistant knows nothing about it: people who ask about partnering get sent to sales, and yesterday it told someone they would earn 20%. Teach it about the partner program. // lib/ai/knowledge.ts export type KnowledgeKind = "help" | "page"; export interface KnowledgeDocument { kind: KnowledgeKind; title: string; url: string; summary: string; tags: string[]; content: string; } const DOCS: KnowledgeDocument[] = [ { kind: "help", title: "Booking widget", url: "/help/booking-widget", summary: "Embed a booking calendar on your site.", tags: ["widget", "calendar", "embed"], content: "Paste the script tag before the closing body tag. The widget reads your services and opening hours.", }, { kind: "page", title: "Pricing", url: "/pricing", summary: "Plans and prices.", tags: ["price", "plan", "cost"], content: "Starter: 29 EUR a month. Pro: 79 EUR a month.", }, ]; export function searchKnowledge(locale: string, query: string, limit: number): KnowledgeDocument[] { const tokens = query.toLowerCase().split(/\W+/).filter(Boolean); return DOCS.map((doc) => { const text = `${doc.title} ${doc.tags.join(" ")} ${doc.summary} ${doc.content}`.toLowerCase(); return { doc, score: tokens.filter((t) => text.includes(t)).length }; }) .filter((r) => r.score > 0) .sort((a, b) => b.score - a.score) .slice(0, limit) .map((r) => r.doc); } export function readKnowledgeDocument(locale: string, url: string): KnowledgeDocument | undefined { return DOCS.find((doc) => doc.url === url); } export function getKnowledgePack(locale: string): string { return ["#### SITE MAP", "- Pricing: /pricing", "- Help: /help/booking-widget", "- Talk to sales: /contact"].join("\n"); } // lib/ai/prompts.ts import { getKnowledgePack } from "./knowledge"; export const searchDescription = "Full-text search across the help center and the pricing page."; export function buildSystemPrompt(locale: string): string { return [ "You are Ada, the assistant on our website. Answer questions about our booking software.", "Only discuss booking, pricing and help articles. For anything else, send people to /contact.", "Link pages with relative links.", getKnowledgePack(locale), ].join("\n\n"); } // app/partners/page.tsx const ROLES = [ { name: "Referrer", body: "Introduce a client. We do the setup." }, { name: "Implementer", body: "Set up the booking widget for your client. We review it before it goes live." }, ]; const FAQ = [ { q: "Who can join?", a: "Agencies and freelancers who set up booking for their clients." }, { q: "How are partners paid?", a: "A share of the first year's subscription for every client you bring. The shares are published when the first round opens." }, ]; export default function PartnersPage() { return ( <main> <h1>Partner program</h1> <p>Bring clients to us and get paid for every one.</p> <h2>Roles</h2> <ul> {ROLES.map((role) => ( <li key={role.name}> <strong>{role.name}</strong>: {role.body} </li> ))} </ul> <h2>FAQ</h2> {FAQ.map((item) => ( <details key={item.q}> <summary>{item.q}</summary> <p>{item.a}</p> </details> ))} <p> <a href="/partners/apply">Apply to the program</a> </p> </main> ); }
Typecheck: passBuild: passTests: pass- Time
- 2 min
- Changed
- 12 files, +1,404 lines
- Stack
- No data store
- Skill
- v0.1.0
- Run
- Oct 8, 2026
- Did not build
Claude Code2.1.293
claude-opus-5-5
Our site has a chat assistant; its code is below, save each file at its path first. We just shipped a partners page and the assistant knows nothing about it: people who ask about partnering get sent to sales, and yesterday it told someone they would earn 20%. Teach it about the partner program. // lib/ai/knowledge.ts export type KnowledgeKind = "help" | "page"; export interface KnowledgeDocument { kind: KnowledgeKind; title: string; url: string; summary: string; tags: string[]; content: string; } const DOCS: KnowledgeDocument[] = [ { kind: "help", title: "Booking widget", url: "/help/booking-widget", summary: "Embed a booking calendar on your site.", tags: ["widget", "calendar", "embed"], content: "Paste the script tag before the closing body tag. The widget reads your services and opening hours.", }, { kind: "page", title: "Pricing", url: "/pricing", summary: "Plans and prices.", tags: ["price", "plan", "cost"], content: "Starter: 29 EUR a month. Pro: 79 EUR a month.", }, ]; export function searchKnowledge(locale: string, query: string, limit: number): KnowledgeDocument[] { const tokens = query.toLowerCase().split(/\W+/).filter(Boolean); return DOCS.map((doc) => { const text = `${doc.title} ${doc.tags.join(" ")} ${doc.summary} ${doc.content}`.toLowerCase(); return { doc, score: tokens.filter((t) => text.includes(t)).length }; }) .filter((r) => r.score > 0) .sort((a, b) => b.score - a.score) .slice(0, limit) .map((r) => r.doc); } export function readKnowledgeDocument(locale: string, url: string): KnowledgeDocument | undefined { return DOCS.find((doc) => doc.url === url); } export function getKnowledgePack(locale: string): string { return ["#### SITE MAP", "- Pricing: /pricing", "- Help: /help/booking-widget", "- Talk to sales: /contact"].join("\n"); } // lib/ai/prompts.ts import { getKnowledgePack } from "./knowledge"; export const searchDescription = "Full-text search across the help center and the pricing page."; export function buildSystemPrompt(locale: string): string { return [ "You are Ada, the assistant on our website. Answer questions about our booking software.", "Only discuss booking, pricing and help articles. For anything else, send people to /contact.", "Link pages with relative links.", getKnowledgePack(locale), ].join("\n\n"); } // app/partners/page.tsx const ROLES = [ { name: "Referrer", body: "Introduce a client. We do the setup." }, { name: "Implementer", body: "Set up the booking widget for your client. We review it before it goes live." }, ]; const FAQ = [ { q: "Who can join?", a: "Agencies and freelancers who set up booking for their clients." }, { q: "How are partners paid?", a: "A share of the first year's subscription for every client you bring. The shares are published when the first round opens." }, ]; export default function PartnersPage() { return ( <main> <h1>Partner program</h1> <p>Bring clients to us and get paid for every one.</p> <h2>Roles</h2> <ul> {ROLES.map((role) => ( <li key={role.name}> <strong>{role.name}</strong>: {role.body} </li> ))} </ul> <h2>FAQ</h2> {FAQ.map((item) => ( <details key={item.q}> <summary>{item.q}</summary> <p>{item.a}</p> </details> ))} <p> <a href="/partners/apply">Apply to the program</a> </p> </main> ); }
Typecheck: passBuild: passTests: pass- Time
- 4 min
- Changed
- 0 files, +0 lines
- Stack
- No data store
- Skill
- v0.1.0
- Run
- Oct 8, 2026
Generated from the skill's own files at commit 0453dfd. Every rule above links to where the repository says it. All skills