Merge pull request #128 from evdboom/ai-instructions-overhaul

Ai instructions overhaul
This commit is contained in:
Erik van der Boom 2026-07-16 06:20:07 +02:00 committed by GitHub
commit a731a5e5ce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 339 additions and 375 deletions

View file

@ -2,7 +2,7 @@
## Highlights
- Standardized release packaging across all supported surfaces.
- Uniformed template generation
- See attached assets below for install-ready downloads.
## Release Assets

View file

@ -64,11 +64,11 @@ const TEMPLATES: Record<string, TemplateModule> = {
// Bump per-file version inside the matching module's `meta` when content
// changes significantly so users with outdated content are prompted.
export const FILE_VERSION_INFO: Record<string, { version: number; label: string; zip: string | null }> =
export const FILE_VERSION_INFO: Record<string, { version: number; label: string; }> =
Object.fromEntries(
Object.values(TEMPLATES).map(t => [
t.meta.file,
{ version: t.meta.version, label: t.meta.label, zip: t.meta.zip },
{ version: t.meta.version, label: t.meta.label },
]),
);

View file

@ -0,0 +1,23 @@
import type { TemplateContext, AgentTemplate } from './context';
import { pushCapabilitiesSource, pushProjectSection, pushSessionStart, pushMemorySystem, pushRepoLayout, pushWritingRules } from './building-blocks';
export type TopLevelTarget = 'claude' | 'copilot' | 'cursor' | 'agents';
export function renderAgentTemplate(ctx: TemplateContext, agent: AgentTemplate): string {
const lines: string[] = [
`# ${agent.name}${ctx.title}`,
'',
];
pushProjectSection(lines, ctx);
lines.push('');
pushSessionStart(lines, ctx, agent);
lines.push('');
pushMemorySystem(lines, agent);
lines.push('');
pushRepoLayout(lines, ctx);
lines.push('');
pushWritingRules(lines, ctx);
lines.push('');
pushCapabilitiesSource(lines);
return lines.filter(l => l !== '\n').join('\n') + '\n';
};

View file

@ -1,59 +1,12 @@
import { audienceNote, languageSection, type TemplateContext, type TemplateMeta } from './context';
import { type TemplateContext, type TemplateMeta } from './context';
import { renderAgentTemplate } from './agentRender';
export const meta: TemplateMeta = {
file: 'AGENTS.md',
version: 11,
label: 'agents instructions',
zip: null,
version: 13,
label: 'agent instructions',
};
export function render(ctx: TemplateContext): string {
const { title, author, description, genre, storyFolder, notesFolder, arcFolder, charactersFolder, sessionFile, preferencesFile, arcGranularity, memoriesFolder } = ctx;
const lines: string[] = [`# Agent Instructions — ${title}`, ''];
lines.push('## Project overview');
if (genre) { lines.push(`${genre} novel.`); }
if (description) { lines.push(description); }
if (ctx.audience){ lines.push(audienceNote(ctx)); }
if (author) { lines.push(`Author: ${author}.`); }
lines.push(
languageSection(ctx),
'',
'## Start of session',
`1. Read \`${sessionFile}\` for current focus and handoff notes (via \`session_focus_get\`), and \`${preferencesFile}\` for the author's durable working preferences.`,
`2. Read \`${memoriesFolder}/global.md\` for cross-chapter context.`,
`3. If working on a specific chapter, read \`${memoriesFolder}/chXX.md\` if it exists.`,
'4. Check `.claude/skills/` for shared slash workflows before improvising a bespoke process.',
'',
'## Story files',
`- Chapter files are \`.md\` files in \`${storyFolder}/\`, organized in act subfolders.`,
`- Arc files live in \`${arcFolder}/\`; default planning granularity is \`${arcGranularity}\`. Treat these as story architecture.`,
`- Character profiles live in \`${charactersFolder}/\`; use one profile per character plus the index.`,
'- HTML comments `<!-- -->` are writer notes — treat as context only, not prose.',
'- Quotation marks and em-dashes are managed by the Bindery extension. Do not normalize them.',
'',
'## Shared skill workflows',
'- Shared workflows live in `.claude/skills/` and can be used by agents beyond Claude when the runtime supports workspace skills.',
'- Prefer `/read-in`, `/review`, `/translation-review`, `/translate`, `/memory`, `/continuity`, `/status`, `/read-aloud`, `/proof-read`, `/plan-beats`, and `/character-setup` when the user is asking for one of those structured tasks.',
'- Use `arc_*` tools for story structure, `character_*` tools for cast profiles, `note_*` tools for story notes, `memory_*` tools for session decisions, `chapter_status_*` tools for progress, and `session_focus_*` tools for current working state. PREFERENCES.md is user-owned — propose changes rather than writing it.',
'- Send rough, unsorted, or pasted material to `Notes/Inbox.md`, then triage with `inbox_process` (propose destinations) and `inbox_resolve` (clear routed items) — do not dump it into memory.',
'',
'## Writing guidelines',
'- Do not rewrite paragraphs unless explicitly asked. Suggest edits only.',
);
if (ctx.audience) {
lines.push(`- Audience is ${ctx.audience}. Keep vocabulary clear and themes age-appropriate.`);
}
lines.push(
'',
'## Key reference files',
'| File | Contains |',
'|---|---|',
`| \`${arcFolder}/\` | Story arc files for overall and per-act structure and beats |`,
`| \`${charactersFolder}/\` | Character index and one profile per character |`,
`| \`${notesFolder}/\` | Story notes, like world rules, scene ideas, inbox, and research |`,
`| \`${sessionFile}\` | Ephemeral working state: current focus, next actions, open questions, handoff (via \`session_focus_*\`) |`,
`| \`${preferencesFile}\` | Durable working preferences; user-owned, never tool-written |`,
`| \`${memoriesFolder}/global.md\` | Cross-session decisions |`,
);
return lines.join('\n') + '\n';
return renderAgentTemplate(ctx, { hasSkills: true, requiresSkillUpload: false, name: 'Agent Instructions' });
}

View file

@ -4,7 +4,6 @@ export const meta: TemplateMeta = {
file: '.bindery/README.md',
version: 10,
label: 'bindery capabilities',
zip: null,
};
export function render(ctx: TemplateContext): string {
@ -27,7 +26,7 @@ may fetch it for context; otherwise, treat this file as the complete answer.
Bindery is a markdown book authoring toolkit with three surfaces:
1. **VS Code / Obsidian extension** commands, format-on-save, export to DOCX/EPUB/PDF.
2. **MCP server** programmatic tools for AI assistants (Claude, Copilot, Codex, Cursor).
2. **MCP server** programmatic tools for AI assistants (Claude, Copilot, ChatGPT, Cursor).
3. **Skill workflows** opinionated slash-commands like \`/review\`, \`/translate\`, \`/memory\`.
All three operate on the same workspace state: \`.bindery/settings.json\`,
@ -121,7 +120,7 @@ tagged **(writes)** modify files or git state.
### Tool Workflow Shortcuts
- Use \`init_workspace\` first for a new book. It creates the settings, translation file, generated capability README, and the default authoring scaffold.
- Use \`setup_ai_files\` after init or when \`health\` reports outdated AI files. It refreshes CLAUDE.md, Copilot instructions, Cursor rules, AGENTS.md, Claude skills, and this capability README.
- Use \`setup_ai_files\` after init or when \`health\` reports outdated AI files. It refreshes CLAUDE.md, Copilot instructions, Cursor rules, AGENTS.md, generated skills, and this capability README.
- Use \`arc_*\` for story architecture under \`${arcFolder}/\`.
- Use \`character_*\` for cast profiles under \`${charactersFolder}/\`.
- Use \`note_*\` for canonical story notes under \`${notesFolder}/\`.
@ -156,7 +155,7 @@ work-in-progress and continue on another machine.
## Skill workflows (Claude / Copilot Chat / etc.)
Generated under \`.claude/skills/<name>/SKILL.md\`. Trigger them with a slash
Generated under \`.claude/skills/<name>/SKILL.md\` and/or \`.agents/skills/<name>/SKILL.md\` depending on selected targets. Trigger them with a slash
command or by paraphrasing the description.
| Skill | Trigger phrases |

View file

@ -0,0 +1,97 @@
import { type TemplateContext, type AgentTemplate } from './context';
/**
* Shared project-intent section used by top-level AI instruction templates.
*/
export function pushProjectSection(lines: string[], ctx: TemplateContext): void {
lines.push('## Project');
if (ctx.genre) {lines.push(`Genre: ${ctx.genre}`); }
if (ctx.description) { lines.push(ctx.description); }
if (ctx.audience) { lines.push(`Target audience: ${ctx.audience}.`); }
if (ctx.author) { lines.push(`Author: ${ctx.author}.`); }
if (ctx.hasMultiLang) { lines.push(`Languages: ${ctx.langList}`); }
}
/**
* Canonical capabilities pointer shared by all top-level AI instruction files.
*/
export function pushCapabilitiesSource(lines: string[]): void {
lines.push(
'## Capabilities source',
'- Use `.bindery/README.md` if you require the canonical source for Bindery capabilities and workflows.',
);
}
/**
* Shared session-start instructions used by top-level AI instruction templates.
*/
export function pushSessionStart(lines: string[], ctx: TemplateContext, agent: AgentTemplate): void {
lines.push(
'## Start of session',
'1. Run `health` from the Bindery MCP and check `ai_versions_outdated`.',
'2. If `ai_versions_outdated` has entries, run `setup_ai_files`.' + (agent.requiresSkillUpload ? ' If `skill_files.reupload_required` has entries, ask the user to re-upload those SKILL.md files to use them.' : ''),
);
if (agent.hasSkills) {
lines.push(
'3. Use /read-in at the start of a session to load context and get your bearings.',
`4. If the skill is not available, read at least ${ctx.sessionFile} (if present, use \`session_focus_get\`) for current focus and handoff context, and ${ctx.preferencesFile} for the author's durable working preferences.`,
);
} else {
lines.push(
`3. Read ${ctx.sessionFile} (if present) for current focus and handoff context, and ${ctx.preferencesFile} for the author's durable working preferences.`,
`4. If ${ctx.sessionFile} mentions a chapter, \`get_chapter(chapterNumber)\` to read that chapter and \`memory_list\` to check for any chapter-specific memory files.`,
);
}
}
/**
* Shared memory-system instructions used by top-level AI instruction templates.
*/
export function pushMemorySystem(lines: string[], agent: AgentTemplate): void {
lines.push('## Memory system');
if (agent.hasSkills) {
lines.push(
`1. When concluding a discussion, or after you give a meaningful, preservation-worthy response: use /memory to store it.`,
`2. Also when the user asks or otherwise indicates the end of a session: use /memory to save decisions.`,
);
} else {
lines.push(
`1. When concluding a discussion, or after you give a meaningful, preservation-worthy response: use \`memory_append\` to store it.`,
`2. Also when the user asks or otherwise indicates the end of a session: use \`memory_append\` to save decisions.`,
`3. If a memory file grows too large, use \`memory_compact\` to condense it.`,
);
}
}
export function pushRepoLayout(lines: string[], ctx: TemplateContext): void {
lines.push(
'## Repository layout',
'```',
`${ctx.arcFolder}/ ← story architecture (${ctx.arcGranularity}-level arc planning by default)`,
' index.md ← arc map',
' Overall.md ← whole-book arc',
' Acts/ ← act-level arc files',
`${ctx.notesFolder}/ ← story notes`,
`${ctx.charactersFolder}/ ← character index and one profile per character`,
`${ctx.sessionFile} ← ephemeral working state (current focus / next actions / open questions / handoff) via session_focus_*`,
`${ctx.preferencesFile} ← durable working preferences ("do it like this for me"); user-owned, never tool-written`,
`${ctx.storyFolder}/`,
...ctx.languages.map(l => ` ${l.folderName}/ ← ${l.code} chapters (one .md per chapter)`),
'```',
);
}
export function pushWritingRules(lines: string[], ctx: TemplateContext): void {
lines.push(
'## Writing rules',
`- If these rules conflict with the author's preferences in ${ctx.preferencesFile}, follow the preferences.`,
'- Prefer usage of the Bindery MCP tools over direct file access.',
'- Never rewrite paragraphs unless explicitly asked. Suggest edits only.',
'- HTML comments `<!-- -->` in chapter files are writer notes. Treat as context, not prose.',
'- Quotation marks and dashes in chapter files are managed by the Bindery extension. Do not flag these as formatting errors.',
);
if (ctx.audience) {
lines.push(`- Content is aimed at ${ctx.audience}. Keep language accessible and themes age-appropriate.`);
}
}

View file

@ -1,121 +1,12 @@
import { audienceNote, languageSection, type TemplateContext, type TemplateMeta } from './context';
import { type TemplateContext, type TemplateMeta } from './context';
import { renderAgentTemplate } from './agentRender';
export const meta: TemplateMeta = {
file: 'CLAUDE.md',
version: 17,
version: 18,
label: 'project instructions',
zip: null,
};
export function render(ctx: TemplateContext): string {
const { title, author, description, genre, storyFolder, notesFolder, arcFolder, charactersFolder, sessionFile, preferencesFile, arcGranularity } = ctx;
const lines: string[] = [
`# Claude — ${title}`,
'',
'## Project',
];
if (genre) { lines.push(`Genre: ${genre}.`); }
if (description) { lines.push(description); }
if (ctx.audience){ lines.push(audienceNote(ctx)); }
if (author) { lines.push(`Author: ${author}.`); }
lines.push(
languageSection(ctx),
'## Start of session',
'1. Use /read-in at the start of a session to load context and get your bearings.',
'2. Run `health` from the Bindery MCP and check `ai_versions_outdated`.',
'3. If `ai_versions_outdated` has entries, run `setup_ai_files`; if `skill_files.reupload_required` has entries, ask the user to re-upload those SKILL.md files in Claude Desktop Skills (no zip needed).',
`4. If the skill or MCP server is not available, read at least ${sessionFile} (if present) for current focus and handoff context, and ${preferencesFile} for the author's durable working preferences.`,
'',
'## Memory system',
'1. When concluding a discussion, or after you give a meaningful, preservation-worthy response: use /memory to store it.',
'2. Also when the user asks or otherwise indicates the end of a session: use /memory to save decisions.',
'',
'## Repo layout',
'```',
`${arcFolder}/ ← story architecture (${arcGranularity}-level arc planning by default)`,
` index.md ← arc map`,
` Overall.md ← whole-book arc`,
` Acts/ ← act-level arc files`,
`${notesFolder}/ ← story notes`,
`${charactersFolder}/ ← character index and one profile per character`,
`${sessionFile} ← ephemeral working state (current focus / next actions / open questions / handoff) via session_focus_*`,
`${preferencesFile} ← durable working preferences ("do it like this for me"); user-owned, never tool-written`,
`${storyFolder}/`,
...ctx.languages.map(l => ` ${l.folderName}/ ← ${l.code} chapters (one .md per chapter)`),
'```',
'',
'## Writing rules',
'- Never rewrite paragraphs unless explicitly asked. Suggest edits only.',
'- HTML comments `<!-- -->` in chapter files are writer notes. Treat as context, not prose.',
'- Quotation marks and dashes in chapter files are managed by the Bindery extension. Do not flag these as formatting errors.',
);
if (ctx.audience) {
lines.push(`- Content is aimed at ${ctx.audience}. Keep language accessible and themes age-appropriate.`);
}
lines.push(
'',
'## Available skills',
'Use these slash commands to trigger structured workflows:',
'| Command | Purpose |',
'|---|---|',
'| `/review` | Review a chapter for language, arc consistency, and age-appropriateness |',
'| `/brainstorm` | Guided story ideation when the author is stuck |',
'| `/plan-beats` | Create, expand, or refine chapter and scene beatmaps |',
'| `/character-setup` | Create or refine structured character profiles |',
'| `/memory` | Update memory files and compact if needed |',
'| `/translate` | Assist with chapter translation |',
'| `/translation-review` | Review a hand-crafted translation against the source |',
'| `/status` | Book progress snapshot |',
'| `/continuity` | Check a chapter for consistency errors |',
'| `/read-aloud` | Test how a passage reads when spoken |',
'| `/read-in` | Load context and get your bearings at the start of a session |',
'| `/proof-read` | Read the book as multiple proofreaders and present the findings |',
'',
'## MCP server (bindery-mcp)',
'',
'All tools require a `book` argument. Use `list_books` to discover available names.',
'Prefer these tools over Read/Bash when they apply.',
'',
'| Tool | What it does |',
'|---|---|',
'| `list_books` | List all configured book names |',
'| `identify_book` | Match a working directory to a book name |',
'| `health` | Server status: settings, index, embedding backend |',
'| `init_workspace` | Create or update `.bindery/settings.json`, `translations.json`, `.bindery/README.md`, and the opinionated Arc / Notes / Characters / SESSION / PREFERENCES / memory / status scaffold |',
'| `setup_ai_files` | Regenerate AI instruction files and return a change manifest |',
'| `index_build` | Build or rebuild the full-text search index |',
'| `index_status` | Show index chunk count and build time |',
'| `get_text` | Read any file by relative path, with optional line range |',
'| `get_chapter` | Full chapter content by number and language |',
'| `get_book_until` | Fetch chapters from 1..N (or start..N) in one call, concatenated in reading order |',
'| `get_overview` | Chapter structure — acts, chapters, titles |',
'| `get_notes` | Notes/ files, filterable by category or name |',
'| `note_list` / `note_get` | List or read canonical story notes under Notes/ |',
'| `note_create` / `note_append` | Create or append canonical story notes under Notes/ |',
'| `character_list` / `character_get` | List or read structured character profiles |',
'| `character_create` / `character_update` | Create or update structured character profiles and the cast index |',
'| `arc_list` / `arc_get` | List or read structured arc files |',
'| `arc_create` / `arc_update` | Create or update structured arc files and the arc index |',
'| `search` | BM25 full-text search with ranked snippets, optional semantic ranking |',
'| `format` | Apply typography formatting to a file or folder |',
'| `get_review_text` | Structured git diff with review-marker regions and optional auto-staging that consumes markers |',
'| `update_workspace` | Fetch and pull the current branch, with branch/default-branch reporting |',
'| `git_snapshot` | Commit changes in bindery workspace, with optional push |',
'| `get_translation` | List glossary entries for a language, or look up a specific term (forgiving) |',
'| `add_translation` | Add or update a cross-language glossary entry (agent reference, not auto-applied) |',
'| `get_dialect` | List dialect substitution rules, or look up a specific word |',
'| `add_dialect` | Add or update a dialect substitution rule (auto-applied at export, e.g. US→UK) |',
'| `add_language` | Add a language to settings.json and scaffold its story folder with stubs |',
'| `settings_update` | Merge a partial patch into settings.json without replacing unrelated keys |',
'| `memory_list` | List `.bindery/memories/` files with line counts |',
'| `memory_append` | Append a dated session entry to a file in `.bindery/memories/` |',
'| `memory_compact` | Overwrite a file in `.bindery/memories/` with a summary (backs up original to `.bindery/memories/archive/`) |',
'| `chapter_status_get` | Read the chapter progress tracker — entries grouped by status |',
'| `chapter_status_update` | Upsert chapter progress entries (send only changed chapters) |',
`| \`session_focus_get\` | Read current working state from ${sessionFile} (optionally one section) |`,
`| \`session_focus_update\` | Update neutral ${sessionFile} sections (Current Focus, Next Actions, Open Questions, Handoff Notes); leaves ${preferencesFile} untouched |`,
`| \`inbox_process\` | Enumerate ${notesFolder}/Inbox.md items with stable numbers and propose destinations (read-only) |`,
`| \`inbox_resolve\` | Remove already-routed inbox items by number after confirmation |`,
);
return lines.filter(l => l !== '\n').join('\n') + '\n';
return renderAgentTemplate(ctx, { hasSkills: true, requiresSkillUpload: true, name: 'Claude' });
}

View file

@ -32,15 +32,10 @@ export interface TemplateMeta {
version: number;
/** Short, human-readable label used by health reporting. */
label: string;
/** Legacy companion zip path or null. */
zip: string | null;
}
export function audienceNote(ctx: TemplateContext): string {
return ctx.audience ? `Target audience: ${ctx.audience}.` : '';
}
export function languageSection(ctx: TemplateContext): string {
if (!ctx.hasMultiLang) { return ''; }
return `\nLanguages: ${ctx.langList}.\n`;
}
export interface AgentTemplate {
hasSkills: boolean;
requiresSkillUpload: boolean;
name: string;
}

View file

@ -1,48 +1,12 @@
import { audienceNote, languageSection, type TemplateContext, type TemplateMeta } from './context';
import { type TemplateContext, type TemplateMeta } from './context';
import { renderAgentTemplate } from './agentRender';
export const meta: TemplateMeta = {
file: '.github/copilot-instructions.md',
version: 11,
version: 12,
label: 'copilot instructions',
zip: null,
};
export function render(ctx: TemplateContext): string {
const { title, author, description, genre, storyFolder, notesFolder, arcFolder, charactersFolder, sessionFile, preferencesFile, arcGranularity } = ctx;
const lines: string[] = [`# GitHub Copilot — ${title}`, ''];
if (genre || description || ctx.audience) {
lines.push('## Project');
if (genre) { lines.push(`${genre} novel.`); }
if (description) { lines.push(description); }
if (ctx.audience){ lines.push(audienceNote(ctx)); }
if (author) { lines.push(`Author: ${author}.`); }
lines.push(languageSection(ctx), '');
}
lines.push(
'## Repo layout',
'```',
`${arcFolder}/ ← story architecture (${arcGranularity}-level arc planning by default)`,
` index.md / Overall.md / Acts/`,
`${notesFolder}/ ← story notes`,
`${charactersFolder}/ ← character index and one profile per character`,
`${sessionFile} ← ephemeral working state (current focus / handoff) via session_focus_*`,
`${preferencesFile} ← durable working preferences; user-owned, never tool-written`,
`${storyFolder}/`,
...ctx.languages.map(l => ` ${l.folderName}/ ← ${l.code} chapters`),
'```',
'',
'## Shared skill workflows',
'- Workspace skill files live in `.claude/skills/` and may also be picked up by agents beyond Claude.',
'- Prefer those shared slash workflows when available: `/read-in`, `/review`, `/translation-review`, `/translate`, `/memory`, `/continuity`, `/status`, `/read-aloud`, `/proof-read`, `/plan-beats`, `/character-setup`.',
'- Treat arc files as story architecture, not generic notes. Use `arc_*` tools for structure, `character_*` tools for durable cast facts, `note_*` tools for story notes, `memory_*` tools for cross-session decisions, and `session_focus_*` tools for current working state. Durable preferences are user-owned in PREFERENCES.md — propose changes rather than writing it.',
'- Send rough, unsorted, or pasted material to `Notes/Inbox.md`, then triage it with `inbox_process` (propose destinations) and `inbox_resolve` (clear routed items) — do not dump it into memory.',
'',
'## Writing guidelines',
'- HTML comments `<!-- -->` in chapter files are writer notes — treat as context only.',
'- Quotation marks and dashes are managed by the Bindery VS Code extension. Do not normalize them.',
);
if (ctx.audience) {
lines.push(`- Content targets ${ctx.audience}. Keep vocabulary accessible and themes appropriate.`);
}
return lines.join('\n') + '\n';
return renderAgentTemplate(ctx, { hasSkills: true, requiresSkillUpload: false, name: 'GitHub Copilot' });
}

View file

@ -1,42 +1,12 @@
import type { TemplateContext, TemplateMeta } from './context';
import { renderAgentTemplate } from './agentRender';
export const meta: TemplateMeta = {
file: '.cursor/rules',
version: 11,
version: 12,
label: 'cursor rules',
zip: null,
};
export function render(ctx: TemplateContext): string {
const { title, storyFolder, notesFolder, arcFolder, charactersFolder, sessionFile, preferencesFile, arcGranularity, memoriesFolder } = ctx;
const lines: string[] = [
`# Cursor rules — ${title}`,
'',
`Story folder: \`${storyFolder}/\``,
`Notes folder: \`${notesFolder}/\``,
`Arc folder: \`${arcFolder}/\` (index.md, Overall.md, Acts/; default granularity: ${arcGranularity})`,
`Characters folder: \`${charactersFolder}/\``,
`Session file: \`${sessionFile}\` (ephemeral working state)`,
`Preferences file: \`${preferencesFile}\` (durable, user-owned)`,
'',
'## Context files to read',
`- \`${sessionFile}\` — ephemeral working state (current focus, next actions, open questions, handoff) via \`session_focus_*\``,
`- \`${preferencesFile}\` — durable working preferences ("do it like this for me"); user-owned, never tool-written`,
`- \`${memoriesFolder}/global.md\` — cross-chapter decisions (read at start of session)`,
`- \`${arcFolder}/\` — story architecture, structure, pacing, and beats`,
`- \`${charactersFolder}/\` — character index and one profile per character`,
`- \`${notesFolder}/\` — story notes, like world rules, scene ideas, inbox, and research`,
'- Shared workflows live in `.claude/skills/`; if your runtime exposes them, prefer `/read-in`, `/review`, `/translation-review`, `/translate`, `/memory`, `/continuity`, `/status`, `/read-aloud`, `/proof-read`, `/plan-beats`, and `/character-setup` for those tasks.',
'- Use `arc_*` tools for story structure, `character_*` tools for cast profiles, `note_*` tools for story notes, `memory_*` tools for session decisions, `chapter_status_*` tools for progress, and `session_focus_*` tools for current working state.',
'- Send rough, unsorted, or pasted material to `Notes/Inbox.md`, then triage with `inbox_process` and `inbox_resolve` — do not dump it into memory.',
'',
'## Rules',
'- HTML comments `<!-- -->` in chapter files are writer notes. Treat as context, not story content.',
'- Do not normalize quotation marks or dashes — these are managed by the Bindery extension.',
'- Do not rewrite prose unless explicitly asked. Suggest edits only.',
];
if (ctx.audience) {
lines.push(`- Target audience is ${ctx.audience}. Flag content that is too complex or inappropriate.`);
}
return lines.join('\n') + '\n';
return renderAgentTemplate(ctx, { hasSkills: false, requiresSkillUpload: false, name: 'Cursor' });
}

View file

@ -4,7 +4,6 @@ export const meta: TemplateMeta = {
file: '.claude/skills/brainstorm/SKILL.md',
version: 12,
label: 'brainstorm skill',
zip: null,
};
const CONTENT = [

View file

@ -4,7 +4,6 @@ export const meta: TemplateMeta = {
file: '.claude/skills/character-setup/SKILL.md',
version: 1,
label: 'character-setup skill',
zip: null,
};
const CONTENT = [

View file

@ -4,7 +4,6 @@ export const meta: TemplateMeta = {
file: '.claude/skills/continuity/SKILL.md',
version: 14,
label: 'continuity skill',
zip: null,
};
const CONTENT = [

View file

@ -4,7 +4,6 @@ export const meta: TemplateMeta = {
file: '.claude/skills/memory/SKILL.md',
version: 14,
label: 'memory skill',
zip: null,
};
const CONTENT = [

View file

@ -4,7 +4,6 @@ export const meta: TemplateMeta = {
file: '.claude/skills/plan-beats/SKILL.md',
version: 3,
label: 'plan-beats skill',
zip: null,
};
const CONTENT = [

View file

@ -4,7 +4,6 @@ export const meta: TemplateMeta = {
file: '.claude/skills/proof-read/SKILL.md',
version: 6,
label: 'proof-read skill',
zip: null,
};
const CONTENT = [

View file

@ -4,7 +4,6 @@ export const meta: TemplateMeta = {
file: '.claude/skills/read-aloud/SKILL.md',
version: 11,
label: 'read-aloud skill',
zip: null,
};
const CONTENT = [

View file

@ -4,7 +4,6 @@ export const meta: TemplateMeta = {
file: '.claude/skills/read-in/SKILL.md',
version: 16,
label: 'read-in skill',
zip: null,
};
const CONTENT = [

View file

@ -4,7 +4,6 @@ export const meta: TemplateMeta = {
file: '.claude/skills/review/SKILL.md',
version: 14,
label: 'review skill',
zip: null,
};
const CONTENT = [

View file

@ -4,7 +4,6 @@ export const meta: TemplateMeta = {
file: '.claude/skills/status/SKILL.md',
version: 14,
label: 'status skill',
zip: null,
};
const CONTENT = [

View file

@ -4,7 +4,6 @@ export const meta: TemplateMeta = {
file: '.claude/skills/translate/SKILL.md',
version: 10,
label: 'translate skill',
zip: null,
};
const CONTENT = [

View file

@ -2,9 +2,8 @@ import type { TemplateContext, TemplateMeta } from '../context';
export const meta: TemplateMeta = {
file: '.claude/skills/translation-review/SKILL.md',
version: 4,
version: 5,
label: 'translation-review skill',
zip: null,
};
const CONTENT = [
@ -73,9 +72,9 @@ const CONTENT = [
"",
"## Output format",
"",
"| Category | Before (target) | After (target) | Reason |",
"|---|---|---|---|",
"| Glossary | Keep context short; bold only the changed words | Suggested wording | Term differs from stored glossary |",
"| Category | Location | Before (target) | After (target) | Reason |",
"|---|---|---|---|---|",
"| Glossary | Line 5 | Keep context short; bold only the changed words | Suggested wording | Term differs from stored glossary |",
"",
"**Category values:** Fidelity · Naturalness · Glossary · Register · Comment",
"",

View file

@ -64,10 +64,9 @@ describe('renderTemplate — claude', () => {
expect(result).toContain('## Project');
expect(result).toContain('## Start of session');
expect(result).toContain('## Memory system');
expect(result).toContain('## Repo layout');
expect(result).toContain('## Repository layout');
expect(result).toContain('## Writing rules');
expect(result).toContain('## Available skills');
expect(result).toContain('## MCP server (bindery-mcp)');
expect(result).toContain('## Capabilities source');
});
it('includes genre, description, audience, and author', () => {
@ -113,8 +112,12 @@ describe('renderTemplate — copilot', () => {
it('contains required sections', () => {
const result = renderTemplate('copilot', makeCtx());
expect(result).toContain('# GitHub Copilot — Test Book');
expect(result).toContain('## Repo layout');
expect(result).toContain('## Writing guidelines');
expect(result).toContain('## Project');
expect(result).toContain('## Start of session');
expect(result).toContain('## Memory system');
expect(result).toContain('## Repository layout');
expect(result).toContain('## Writing rules');
expect(result).toContain('## Capabilities source');
});
it('includes genre and description when set', () => {
@ -123,9 +126,12 @@ describe('renderTemplate — copilot', () => {
expect(result).toContain('A tale of adventure.');
});
it('omits project section entirely when all optional fields are empty', () => {
it('keeps project section even when optional project fields are empty', () => {
const result = renderTemplate('copilot', makeMinimalCtx());
expect(result).not.toContain('## Project');
expect(result).toContain('## Project');
expect(result).not.toContain('Genre:');
expect(result).not.toContain('Author:');
expect(result).not.toContain('Target audience:');
});
it('includes audience writing guideline when audience is set', () => {
@ -133,13 +139,10 @@ describe('renderTemplate — copilot', () => {
expect(result).toContain('middle-grade');
});
it('mentions shared workspace skills from the .claude folder', () => {
it('uses read-in startup guidance and no explicit skill catalog', () => {
const result = renderTemplate('copilot', makeCtx());
expect(result).toContain('.claude/skills/');
expect(result).toContain('/translation-review');
expect(result).toContain('/read-in');
expect(result).toContain('/plan-beats');
expect(result).toContain('/character-setup');
expect(result).not.toContain('.claude/skills/');
});
it('ends with a newline', () => {
@ -150,14 +153,19 @@ describe('renderTemplate — copilot', () => {
describe('renderTemplate — cursor', () => {
it('contains required sections', () => {
const result = renderTemplate('cursor', makeCtx());
expect(result).toContain('# Cursor rules — Test Book');
expect(result).toContain('## Context files to read');
expect(result).toContain('## Rules');
expect(result).toContain('# Cursor — Test Book');
expect(result).toContain('## Project');
expect(result).toContain('## Start of session');
expect(result).toContain('## Memory system');
expect(result).toContain('## Repository layout');
expect(result).toContain('## Writing rules');
expect(result).toContain('## Capabilities source');
});
it('references memory and arc folder paths', () => {
it('references arc folder and memory tools', () => {
const result = renderTemplate('cursor', makeCtx());
expect(result).toContain('.bindery/memories');
expect(result).toContain('memory_append');
expect(result).toContain('memory_compact');
expect(result).toContain('Arc/');
});
@ -166,13 +174,11 @@ describe('renderTemplate — cursor', () => {
expect(result).toContain('middle-grade');
});
it('mentions shared workspace skills from the .claude folder', () => {
it('uses non-skill startup guidance for runtimes without skill support', () => {
const result = renderTemplate('cursor', makeCtx());
expect(result).toContain('.claude/skills/');
expect(result).toContain('/translation-review');
expect(result).toContain('/proof-read');
expect(result).toContain('/plan-beats');
expect(result).toContain('/character-setup');
expect(result).toContain('Read SESSION.md');
expect(result).toContain('get_chapter(chapterNumber)');
expect(result).not.toContain('/read-in');
});
it('ends with a newline', () => {
@ -184,11 +190,12 @@ describe('renderTemplate — agents', () => {
it('contains required sections', () => {
const result = renderTemplate('agents', makeCtx());
expect(result).toContain('# Agent Instructions — Test Book');
expect(result).toContain('## Project overview');
expect(result).toContain('## Project');
expect(result).toContain('## Start of session');
expect(result).toContain('## Story files');
expect(result).toContain('## Writing guidelines');
expect(result).toContain('## Key reference files');
expect(result).toContain('## Memory system');
expect(result).toContain('## Repository layout');
expect(result).toContain('## Writing rules');
expect(result).toContain('## Capabilities source');
});
it('includes genre, description, audience, and author', () => {
@ -199,19 +206,21 @@ describe('renderTemplate — agents', () => {
expect(result).toContain('Jane Doe');
});
it('references memories and arc folders', () => {
it('references repository structure and arc folders', () => {
const result = renderTemplate('agents', makeCtx());
expect(result).toContain('.bindery/memories');
expect(result).toContain('Repository layout');
expect(result).toContain('Arc/');
});
it('mentions shared workspace skills from the .claude folder', () => {
it('uses read-in startup guidance and no explicit shared-skill inventory section', () => {
const result = renderTemplate('agents', makeCtx());
expect(result).toContain('.claude/skills/');
expect(result).toContain('/translation-review');
expect(result).toContain('/review');
expect(result).toContain('/plan-beats');
expect(result).toContain('/character-setup');
expect(result).toContain('/read-in');
expect(result).not.toContain('## Shared skill workflows');
});
it('uses skill-first startup guidance with read-in', () => {
const result = renderTemplate('agents', makeCtx());
expect(result).toContain('Use /read-in at the start of a session');
});
it('ends with a newline', () => {

View file

@ -2,7 +2,8 @@
* AI instruction file generation for Bindery (MCP server).
*
* Generates CLAUDE.md, .github/copilot-instructions.md, .cursor/rules,
* AGENTS.md, and .claude/skills/<skill>/SKILL.md from the book's
* AGENTS.md, and skills under .claude/skills/<skill>/SKILL.md and
* .agents/skills/<skill>/SKILL.md from the book's
* .bindery/settings.json.
*
* Template content is maintained in bindery-core/src/templates/*.ts
@ -58,25 +59,21 @@ export interface AiSetupOptions {
export interface AiSetupResult {
regenerated: string[];
skipped: string[];
skillZipManifest: {
rebuilt: string[];
created: string[];
skipped: string[];
failed: string[];
};
versionStamp: AiVersionFile;
}
interface AiVersionEntry {
version: number;
label: string;
zip: string | null;
}
export interface AiVersionFile {
versions: Record<string, AiVersionEntry>;
}
const SKILL_ROOTS = ['.claude', '.agents'] as const;
const SKILL_PATH_RE = /^\.(claude|agents)\/skills\/([^/]+)\/SKILL\.md$/;
// ─── Settings types ───────────────────────────────────────────────────────────
interface Settings extends Omit<WorkspaceSettings, 'languages'> {
@ -142,7 +139,6 @@ export function setupAiFiles(options: AiSetupOptions): AiSetupResult {
const result: AiSetupResult = {
regenerated: [],
skipped: [],
skillZipManifest: { rebuilt: [], created: [], skipped: [], failed: [] },
versionStamp: versionFile,
};
@ -169,6 +165,10 @@ export function setupAiFiles(options: AiSetupOptions): AiSetupResult {
break;
case 'agents':
writeFile(root, 'AGENTS.md', renderTemplate('agents', ctx), overwrite, versionFile, result);
for (const skill of skills) {
const skillMd = path.join('.agents', 'skills', skill, 'SKILL.md');
writeFile(root, skillMd, renderTemplate(skill, ctx), overwrite, versionFile, result);
}
break;
}
}
@ -200,7 +200,10 @@ export function readAiVersionFile(root: string): AiVersionFile {
export function expectedAiVersionEntries(): Record<string, AiVersionEntry> {
const out: Record<string, AiVersionEntry> = {};
for (const [file, info] of Object.entries(FILE_VERSION_INFO)) {
out[file] = { version: info.version, label: info.label, zip: info.zip };
const entry = { version: info.version, label: info.label };
for (const variant of versionPathVariants(file)) {
out[variant] = entry;
}
}
return out;
}
@ -222,7 +225,6 @@ export function writeBinderyCapabilitiesReadme(root: string): void {
const versionFile = readAiVersionFile(root);
const result: AiSetupResult = {
regenerated: [], skipped: [],
skillZipManifest: { rebuilt: [], created: [], skipped: [], failed: [] },
versionStamp: versionFile,
};
writeFile(root, path.join('.bindery', 'README.md'), renderTemplate('bindery-readme', ctx), true, versionFile, result);
@ -241,7 +243,7 @@ function writeFile(
): void {
const full = path.join(root, relPath);
const key = toKey(relPath);
const expected = FILE_VERSION_INFO[key];
const expected = expectedVersionInfoForKey(key);
const existingVersion = versionFile.versions[key]?.version ?? 0;
const isUpToDate = expected ? existingVersion >= expected.version : true;
@ -258,15 +260,29 @@ function writeFile(
}
function stampVersionEntry(versionFile: AiVersionFile, relPath: string): void {
const expected = FILE_VERSION_INFO[relPath];
const expected = expectedVersionInfoForKey(relPath);
if (!expected) { return; }
versionFile.versions[relPath] = {
version: expected.version,
label: expected.label,
zip: expected.zip,
label: expected.label
};
}
function expectedVersionInfoForKey(key: string): { version: number; label: string } | undefined {
for (const variant of versionPathVariants(key)) {
const expected = FILE_VERSION_INFO[variant];
if (expected) { return expected; }
}
return undefined;
}
function versionPathVariants(relPath: string): string[] {
const skillMatch = SKILL_PATH_RE.exec(relPath);
if (!skillMatch) { return [relPath]; }
const skillName = skillMatch[2];
return SKILL_ROOTS.map(root => `${root}/skills/${skillName}/SKILL.md`);
}
function stampAiVersionFile(root: string, versionFile: AiVersionFile): void {
const dir = path.join(root, '.bindery');
fs.mkdirSync(dir, { recursive: true });

View file

@ -446,7 +446,7 @@ function getSemanticStatus(root: string): SemanticStatus {
return { status, staleReasons: stale.isStale ? stale.reasons : [] };
}
type OutdatedEntry = { file: string; label: string; zip: string | null; expected: number; found: number };
type OutdatedEntry = { file: string; label: string; expected: number; found: number };
function getOutdatedAiFiles(root: string, enabledTargets: Set<string>): OutdatedEntry[] {
const installed = readAiVersionFile(root);
@ -457,7 +457,7 @@ function getOutdatedAiFiles(root: string, enabledTargets: Set<string>): Outdated
if (!fs.existsSync(path.join(root, file))) { continue; }
const found = installed.versions[file]?.version ?? 0;
if (found < exp.version) {
outdated.push({ file, label: exp.label, zip: exp.zip, expected: exp.version, found });
outdated.push({ file, label: exp.label, expected: exp.version, found });
}
}
return outdated;
@ -2626,13 +2626,6 @@ export function toolSetupAiFiles(root: string, args: SetupAiFilesArgs): string {
const response = {
regenerated_files: result.regenerated,
skipped_files: result.skipped,
skill_zips: {
created: result.skillZipManifest.created,
rebuilt: result.skillZipManifest.rebuilt,
skipped: result.skillZipManifest.skipped,
failed: result.skillZipManifest.failed,
reupload_required: [],
},
skill_files: {
reupload_required: skillFilesToReupload,
},

View file

@ -21,6 +21,11 @@ async function loadAiSetup() {
return import('../src/aisetup');
}
async function loadTemplates() {
vi.resetModules();
return import('../src/templates');
}
afterEach(() => {
vi.restoreAllMocks();
vi.resetModules();
@ -52,29 +57,6 @@ describe('setupAiFiles skill generation', () => {
expect(fs.existsSync(path.join(root, '.claude', 'skills', 'translation-review.zip'))).toBe(false);
});
it('returns an empty skill zip manifest (zips are no longer generated)', async () => {
const { setupAiFiles } = await loadAiSetup();
const root = makeRoot();
write(path.join(root, '.bindery', 'settings.json'), JSON.stringify({
bookTitle: 'Test Book',
storyFolder: 'Story',
languages: [{ code: 'EN', folderName: 'EN' }],
}, null, 2) + '\n');
const result = setupAiFiles({
root,
targets: ['claude'],
skills: ['read-aloud'],
overwrite: true,
});
expect(result.skillZipManifest.created).toEqual([]);
expect(result.skillZipManifest.rebuilt).toEqual([]);
expect(result.skillZipManifest.skipped).toEqual([]);
expect(result.skillZipManifest.failed).toEqual([]);
});
it('does not overwrite legacy zip files that already exist', async () => {
const { setupAiFiles } = await loadAiSetup();
@ -87,15 +69,86 @@ describe('setupAiFiles skill generation', () => {
write(path.join(root, '.claude', 'skills', 'read-aloud.zip'), 'legacy zip bytes');
const result = setupAiFiles({
setupAiFiles({
root,
targets: ['claude'],
skills: ['read-aloud'],
overwrite: true,
});
expect(result.skillZipManifest.failed).toEqual([]);
expect(result.skillZipManifest.created).toEqual([]);
expect(fs.readFileSync(path.join(root, '.claude', 'skills', 'read-aloud.zip'), 'utf-8')).toBe('legacy zip bytes');
});
it('generates requested skill markdown files for agents target under .agents/skills', async () => {
const { setupAiFiles } = await loadAiSetup();
const root = makeRoot();
write(path.join(root, '.bindery', 'settings.json'), JSON.stringify({
bookTitle: 'Test Book',
storyFolder: 'Story',
languages: [{ code: 'EN', folderName: 'EN' }],
}, null, 2) + '\n');
const result = setupAiFiles({
root,
targets: ['agents'],
skills: ['read-in'],
overwrite: true,
});
expect(result.regenerated).toContain('.agents/skills/read-in/SKILL.md');
expect(fs.existsSync(path.join(root, '.agents', 'skills', 'read-in', 'SKILL.md'))).toBe(true);
});
it('includes .agents skill files in expected AI version entries', async () => {
const { expectedAiVersionEntries } = await loadAiSetup();
const { FILE_VERSION_INFO } = await loadTemplates();
const expected = expectedAiVersionEntries();
const claudeKey = '.claude/skills/read-in/SKILL.md';
const agentsKey = '.agents/skills/read-in/SKILL.md';
expect(expected[agentsKey]).toEqual(expected[claudeKey]);
expect(expected[agentsKey]).toEqual({
version: FILE_VERSION_INFO[claudeKey].version,
label: FILE_VERSION_INFO[claudeKey].label,
});
});
it('regenerates .agents skill file when stamped version is stale and overwrite is false', async () => {
const { setupAiFiles, readAiVersionFile } = await loadAiSetup();
const root = makeRoot();
write(path.join(root, '.bindery', 'settings.json'), JSON.stringify({
bookTitle: 'Test Book',
storyFolder: 'Story',
languages: [{ code: 'EN', folderName: 'EN' }],
}, null, 2) + '\n');
setupAiFiles({
root,
targets: ['agents'],
skills: ['read-in'],
overwrite: true,
});
const versionPath = path.join(root, '.bindery', 'ai-version.json');
const raw = JSON.parse(fs.readFileSync(versionPath, 'utf-8')) as {
versions: Record<string, { version: number; label: string }>;
};
raw.versions['.agents/skills/read-in/SKILL.md'].version = 0;
fs.writeFileSync(versionPath, JSON.stringify(raw, null, 2) + '\n', 'utf-8');
const second = setupAiFiles({
root,
targets: ['agents'],
skills: ['read-in'],
overwrite: false,
});
expect(second.regenerated).toContain('.agents/skills/read-in/SKILL.md');
const stamped = readAiVersionFile(root);
expect(stamped.versions['.agents/skills/read-in/SKILL.md'].version).toBeGreaterThan(0);
});
});

View file

@ -342,18 +342,12 @@ describe('mcp tools', () => {
const parsed = JSON.parse(raw) as {
regenerated_files?: string[];
skipped_files?: string[];
skill_zips?: { created?: string[]; rebuilt?: string[]; skipped?: string[]; failed?: string[]; reupload_required?: string[] };
skill_files?: { reupload_required?: string[] };
ai_versions?: { versions?: Record<string, { version: number; label: string; zip: string | null }> };
ai_versions?: { versions?: Record<string, { version: number; label: string; }> };
};
expect(Array.isArray(parsed.regenerated_files)).toBe(true);
expect(Array.isArray(parsed.skipped_files)).toBe(true);
expect(Array.isArray(parsed.skill_zips?.created)).toBe(true);
expect(Array.isArray(parsed.skill_zips?.rebuilt)).toBe(true);
expect(Array.isArray(parsed.skill_zips?.skipped)).toBe(true);
expect(Array.isArray(parsed.skill_zips?.failed)).toBe(true);
expect(Array.isArray(parsed.skill_zips?.reupload_required)).toBe(true);
expect(Array.isArray(parsed.skill_files?.reupload_required)).toBe(true);
expect(parsed.skill_files?.reupload_required).toContain('.claude/skills/review/SKILL.md');
expect(parsed.ai_versions?.versions?.['.claude/skills/review/SKILL.md']?.label).toBe('review skill');
@ -369,16 +363,11 @@ describe('mcp tools', () => {
const raw = toolSetupAiFiles(root, { targets: ['claude'], skills: ['read-aloud'], overwrite: true });
const parsed = JSON.parse(raw) as {
skill_zips?: { created?: string[]; rebuilt?: string[]; failed?: string[]; reupload_required?: string[] };
skill_files?: { reupload_required?: string[] };
};
const zipPath = path.join(root, '.claude', 'skills', 'read-aloud.zip');
expect(fs.existsSync(zipPath)).toBe(false);
expect(parsed.skill_zips?.created).toEqual([]);
expect(parsed.skill_zips?.rebuilt).toEqual([]);
expect(parsed.skill_zips?.failed).toEqual([]);
expect(parsed.skill_zips?.reupload_required).toEqual([]);
expect(parsed.skill_files?.reupload_required).toEqual(['.claude/skills/read-aloud/SKILL.md']);
});
@ -401,7 +390,7 @@ describe('mcp tools', () => {
// Downgrade the review skill version to simulate outdated
const versionPath = path.join(root, '.bindery', 'ai-version.json');
const versionFile = JSON.parse(fs.readFileSync(versionPath, 'utf-8')) as {
versions: Record<string, { version: number; label: string; zip: string | null }>;
versions: Record<string, { version: number; label: string; }>;
};
versionFile.versions['.claude/skills/review/SKILL.md'].version = 0;
fs.writeFileSync(versionPath, JSON.stringify(versionFile, null, 2) + '\n', 'utf-8');
@ -436,7 +425,7 @@ describe('mcp tools', () => {
// Downgrade the review skill version to simulate outdated
const versionPath = path.join(root, '.bindery', 'ai-version.json');
const versionFile = JSON.parse(fs.readFileSync(versionPath, 'utf-8')) as {
versions: Record<string, { version: number; label: string; zip: string | null }>;
versions: Record<string, { version: number; label: string;}>;
};
versionFile.versions['.claude/skills/review/SKILL.md'].version = 0;
fs.writeFileSync(versionPath, JSON.stringify(versionFile, null, 2) + '\n', 'utf-8');
@ -523,7 +512,7 @@ describe('mcp tools', () => {
const versionPath = path.join(root, '.bindery', 'ai-version.json');
const versionFile = JSON.parse(fs.readFileSync(versionPath, 'utf-8')) as {
versions: Record<string, { version: number; label: string; zip: string | null }>;
versions: Record<string, { version: number; label: string; }>;
};
versionFile.versions['.claude/skills/review/SKILL.md'].version = 0;
fs.writeFileSync(versionPath, JSON.stringify(versionFile, null, 2) + '\n', 'utf-8');
@ -531,7 +520,7 @@ describe('mcp tools', () => {
const healthRaw = toolHealth(root);
const health = JSON.parse(healthRaw) as {
ai_version_outdated?: boolean;
ai_versions_outdated?: Array<{ file: string; label: string; zip: string | null }>;
ai_versions_outdated?: Array<{ file: string; label: string; }>;
};
expect(health.ai_version_outdated).toBe(true);

View file

@ -2,7 +2,7 @@
* Obsidian-specific AI setup wrapper.
*
* Generates AI instruction files (CLAUDE.md, copilot-instructions.md, .cursor/rules, AGENTS.md)
* and Claude skill templates from the book's .bindery/settings.json.
* and skill templates from the book's .bindery/settings.json.
*
* For Obsidian, this is a simpler adaptation since we don't have the VS Code LM API,
* so we just generate the files to the filesystem directly.
@ -89,6 +89,10 @@ export function setupAiFiles(
break;
case 'agents':
writeFile(bookRoot, 'AGENTS.md', renderTemplate('agents', ctx), overwrite, result);
for (const skill of skills) {
const skillDir = path.join('.agents', 'skills', skill);
writeFile(bookRoot, path.join(skillDir, 'SKILL.md'), renderTemplate(skill, ctx), overwrite, result);
}
break;
}
}

View file

@ -85,6 +85,22 @@ describe('AI Setup', () => {
expect(fs.existsSync(agentsPath)).toBe(true);
});
it('should create skill markdown files under .agents/skills when agents target requested', async () => {
const settingsPath = path.join(tempRoot, '.bindery', 'settings.json');
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
fs.writeFileSync(settingsPath, JSON.stringify({
bookTitle: 'Test Book',
}, null, 2), 'utf-8');
await setupAiFiles(mockApp, tempRoot, ['agents'], ['read-in', 'review'], false);
const readInSkillPath = path.join(tempRoot, '.agents', 'skills', 'read-in', 'SKILL.md');
const reviewSkillPath = path.join(tempRoot, '.agents', 'skills', 'review', 'SKILL.md');
expect(fs.existsSync(readInSkillPath)).toBe(true);
expect(fs.existsSync(reviewSkillPath)).toBe(true);
});
it('should skip existing files when overwrite is false', async () => {
const settingsPath = path.join(tempRoot, '.bindery', 'settings.json');
fs.mkdirSync(path.dirname(settingsPath), { recursive: true });

View file

@ -7,7 +7,7 @@
* claude CLAUDE.md + .claude/skills/<skill>/SKILL.md
* copilot .github/copilot-instructions.md
* cursor .cursor/rules
* agents AGENTS.md (OpenAI Agents, Aider, Codex, etc.)
* agents AGENTS.md + .agents/skills/<skill>/SKILL.md (OpenAI Agents, Aider, Codex, etc.)
*
* Templates and TemplateContext live in @bindery/core (single source of truth).
*/
@ -92,6 +92,10 @@ export function setupAiFiles(options: AiSetupOptions): AiSetupResult {
case 'agents':
writeFile(root, 'AGENTS.md', renderTemplate('agents', ctx), overwrite, result);
for (const skill of skills) {
const skillDir = path.join('.agents', 'skills', skill);
writeFile(root, path.join(skillDir, 'SKILL.md'), renderTemplate(skill, ctx), overwrite, result);
}
break;
}
}