feat: add translation-review skill and update related documentation

This commit is contained in:
Erik van der Boom 2026-04-26 22:04:19 +02:00
parent d4f8edc6d9
commit cbcd066588
10 changed files with 203 additions and 12 deletions

View file

@ -118,7 +118,7 @@ see `mcp-ts/src/index.ts` for implementation details and input schemas and `mcpb
| `cursor` | `.cursor/rules` |
| `agents` | `AGENTS.md` |
Skills: `review`, `brainstorm`, `memory`, `translate`, `status`, `continuity`, `read-aloud`, `read-in`, `proof-read`.
Skills: `review`, `brainstorm`, `memory`, `translate`, `translation-review`, `status`, `continuity`, `read-aloud`, `read-in`, `proof-read`.
The **memory skill** uses `memory_list``memory_append``memory_compact`. Do not fall back to `get_text` + Edit tool for memory writes.

View file

@ -23,6 +23,7 @@ export type SkillTemplate =
| 'brainstorm'
| 'memory'
| 'translate'
| 'translation-review'
| 'status'
| 'continuity'
| 'read-aloud'
@ -30,7 +31,7 @@ export type SkillTemplate =
| 'proof-read';
export const ALL_SKILLS: SkillTemplate[] = [
'review', 'brainstorm', 'memory', 'translate', 'status', 'continuity', 'read-aloud', 'read-in', 'proof-read',
'review', 'brainstorm', 'memory', 'translate', 'translation-review', 'status', 'continuity', 'read-aloud', 'read-in', 'proof-read',
];
export interface AiSetupOptions {

View file

@ -417,7 +417,7 @@ server.registerTool('setup_ai_files', {
inputSchema: {
book: bookSchema,
targets: z.array(z.string()).optional().describe('Which files to generate: claude, copilot, cursor, agents. Default: all.'),
skills: z.array(z.string()).optional().describe('Which Claude skills to generate: review, brainstorm, memory, translate, status, continuity, read-aloud, read-in, proof-read. Default: all.'),
skills: z.array(z.string()).optional().describe('Which Claude skills to generate: review, brainstorm, memory, translate, translation-review, status, continuity, read-aloud, read-in, proof-read. Default: all.'),
overwrite: z.boolean().optional().describe('Overwrite existing files? Default false (skip existing).'),
},
annotations: { destructiveHint: true },

View file

@ -30,14 +30,15 @@ export interface TemplateContext {
// Bump FILE_VERSION_INFO[key].version so users with outdated content are prompted.
export const FILE_VERSION_INFO: Record<string, { version: number; label: string; zip: string | null }> = {
'CLAUDE.md': { version: 9, label: 'project instructions', zip: null },
'.github/copilot-instructions.md': { version: 7, label: 'copilot instructions', zip: null },
'.cursor/rules': { version: 7, label: 'cursor rules', zip: null },
'AGENTS.md': { version: 7, label: 'agents instructions', zip: null },
'.claude/skills/review/SKILL.md': { version: 11, label: 'review skill', zip: '.claude/skills/review.zip' },
'CLAUDE.md': { version: 10, label: 'project instructions', zip: null },
'.github/copilot-instructions.md': { version: 8, label: 'copilot instructions', zip: null },
'.cursor/rules': { version: 8, label: 'cursor rules', zip: null },
'AGENTS.md': { version: 8, label: 'agents instructions', zip: null },
'.claude/skills/review/SKILL.md': { version: 12, label: 'review skill', zip: '.claude/skills/review.zip' },
'.claude/skills/brainstorm/SKILL.md': { version: 11, label: 'brainstorm skill', zip: '.claude/skills/brainstorm.zip' },
'.claude/skills/memory/SKILL.md': { version: 11, label: 'memory skill', zip: '.claude/skills/memory.zip' },
'.claude/skills/translate/SKILL.md': { version: 9, label: 'translate skill', zip: '.claude/skills/translate.zip' },
'.claude/skills/translation-review/SKILL.md': { version: 1, label: 'translation-review skill', zip: '.claude/skills/translation-review.zip' },
'.claude/skills/status/SKILL.md': { version: 10, label: 'status skill', zip: '.claude/skills/status.zip' },
'.claude/skills/continuity/SKILL.md': { version: 12, label: 'continuity skill', zip: '.claude/skills/continuity.zip' },
'.claude/skills/read-aloud/SKILL.md': { version: 10, label: 'read-aloud skill', zip: '.claude/skills/read-aloud.zip' },
@ -52,7 +53,7 @@ export const FILE_VERSION_INFO: Record<string, { version: number; label: string;
*
* Top-level file templates: 'claude', 'copilot', 'cursor', 'agents'
* Skill templates: 'review', 'brainstorm', 'memory', 'translate',
* 'status', 'continuity', 'read-aloud', 'read-in', 'proof-read'
* 'translation-review', 'status', 'continuity', 'read-aloud', 'read-in', 'proof-read'
*/
export function renderTemplate(name: string, ctx: TemplateContext): string {
switch (name) {
@ -64,6 +65,7 @@ export function renderTemplate(name: string, ctx: TemplateContext): string {
case 'brainstorm': return skillBrainstorm();
case 'memory': return skillMemory();
case 'translate': return skillTranslate();
case 'translation-review': return skillTranslationReview();
case 'status': return skillStatus();
case 'continuity': return skillContinuity();
case 'read-aloud': return skillReadAloud();
@ -135,6 +137,7 @@ function claudeMd(ctx: TemplateContext): string {
'| `/brainstorm` | Generate plot/character/scene ideas |',
'| `/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 |',
@ -200,6 +203,10 @@ function copilotMd(ctx: TemplateContext): string {
...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`.',
'',
'## 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.',
@ -222,7 +229,8 @@ function cursorRules(ctx: TemplateContext): string {
'## Context files to read',
`- \`${memoriesFolder}/global.md\` — cross-chapter decisions (read at start of session)`,
`- \`${arcFolder}/\` — story arc files for overall and per-act structure and beats`,
`- \`${notesFolder}/\` — story notes, like character profiles and world rules`,
`- \`${notesFolder}/\` — story notes, like character profiles and world rules`,
'- Shared workflows live in `.claude/skills/`; if your runtime exposes them, prefer `/read-in`, `/review`, `/translation-review`, `/translate`, `/memory`, `/continuity`, `/status`, `/read-aloud`, and `/proof-read` for those tasks.',
'',
'## Rules',
'- HTML comments `<!-- -->` in chapter files are writer notes. Treat as context, not story content.',
@ -249,12 +257,17 @@ function agentsMd(ctx: TemplateContext): string {
'## Start of session',
`1. Read \`${memoriesFolder}/global.md\` for cross-chapter context.`,
`2. If working on a specific chapter, read \`${memoriesFolder}/chXX.md\` if it exists.`,
'3. 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.`,
'- 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`, and `/proof-read` when the user is asking for one of those structured tasks.',
'',
'## Writing guidelines',
'- Do not rewrite paragraphs unless explicitly asked. Suggest edits only.',
);
@ -316,6 +329,7 @@ Load the right context, pick any or all as needed:
- Use \`get_chapter\` to load the chapter
- For a Full review, read the relevant arc file from \`Arc/\`.
- For "review my changes", use \`get_review_text\` to get the diff
- If the diff includes translated chapter files, flag that and offer \`/translation-review\` for source-vs-translation feedback
### 2. Perform the review
@ -522,6 +536,82 @@ When the user confirms a new or corrected term translation, call \`add_translati
`;
}
function skillTranslationReview(): string {
return `---
name: translation-review
description: Bindery workspace - Review a hand-crafted translation against the source language for fidelity, naturalness, and glossary consistency. Use for /translation-review, "review my translation", or "what do you think" when translation is the current focus.
---
# Skill: /translation-review
Review a hand-crafted translation against the source.
Use this when the user has written or updated the target-language text and wants structured feedback.
## Prerequisites
This skill requires a Bindery workspace. If unsure, call \`identify_book\` to check.
## Trigger
User says \`/translation-review\`, "review my translation", or "what do you think" when translation is the active focus.
## Not this skill
- Generating translation text from scratch -> use \`/translate\`
- Reviewing source-language writing quality -> use \`/review\`
## Tools
Use these Bindery MCP tools:
- \`get_review_text(autoStage: true, contextLines?: number)\` — gather newly changed lines and auto-stage reviewed changes
- \`get_text(identifier, startLine?, endLine?)\` — fetch matching source lines or focused ranges
- \`get_translation(language)\` — load glossary terms for the target language before reviewing
- \`get_chapter(chapterNumber, language)\` — full chapter source/target pair for full spot-check mode
- \`search(query, language)\` — verify how a term was used in previously translated chapters before flagging it
- \`add_translation(targetLangCode, from, to)\` — persist a confirmed glossary correction
## Mode 1 - Scoped diff review (primary)
### Steps
1. Call \`get_review_text(autoStage: true)\`.
2. If the diff is empty, report that nothing new has been translated yet.
3. Identify changed files and determine source/target language from available context: session file (for example COWORK.md), recent conversation, or ask the user if ambiguous.
4. If the target-language file changed, capture the changed target line range.
5. Use line parity (source line N = target line N) and call \`get_text\` for the same range in the source file.
6. Load glossary entries via \`get_translation(targetLanguage)\`.
7. Use \`search(query, targetLanguage)\` when a term may have an established translation elsewhere in the book.
8. Compare source vs target and produce feedback using the table below.
9. If source-language lines also changed, flag that and suggest \`/review\` for source-quality feedback.
## Mode 2 - Full chapter spot-check
Use this when the user asks for a full chapter comparison.
1. Determine source language, target language, and chapter number.
2. Load glossary with \`get_translation(targetLanguage)\`.
3. Use \`search(query, targetLanguage)\` as needed to verify recurring terminology in earlier translated chapters.
4. Load chapters with \`get_chapter(chapterNumber, sourceLanguage)\` and \`get_chapter(chapterNumber, targetLanguage)\`.
5. Compare paragraph by paragraph and report findings with the same table.
## Output format
| Before (target) | After (target) | Reason |
|---|---|---|
| Keep context short; bold only the changed words | Suggested wording | Fidelity, naturalness, glossary, or terminology consistency |
Also list glossary mismatches and untranslated world-specific terms explicitly.
## Cross-skill handoff
- If changed lines are only source-language files, suggest switching to \`/review\`.
- If both source and target changed, run translation-review findings first, then prompt whether to run \`/review\` for source edits too.
## Rules
- Load glossary before reviewing and flag mismatches explicitly
- Suggest edits only; do not rewrite entire passages unless asked
- Bold only changed words in Before/After rows
- Mark uncertain calls as questions for user confirmation
- When the user confirms a corrected term, call \`add_translation\` before moving on
- Respond in the session language (usually source language)
`;
}
function skillStatus(): string {
return `---
name: status

View file

@ -32,6 +32,27 @@ afterEach(() => {
});
describe('setupAiFiles zip generation', () => {
it('builds the translation-review skill zip when requested', 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' }, { code: 'NL', folderName: 'NL' }],
}, null, 2) + '\n');
const result = setupAiFiles({
root,
targets: ['claude'],
skills: ['translation-review'],
overwrite: true,
});
expect(result.skillZipManifest.created).toContain('.claude/skills/translation-review.zip');
expect(result.regenerated).toContain('.claude/skills/translation-review/SKILL.md');
});
it('builds skill zips in-process with normalized entry paths', async () => {
const { setupAiFiles } = await loadAiSetup();

View file

@ -125,6 +125,13 @@ describe('renderTemplate — copilot', () => {
expect(result).toContain('middle-grade');
});
it('mentions shared workspace skills from the .claude folder', () => {
const result = renderTemplate('copilot', makeCtx());
expect(result).toContain('.claude/skills/');
expect(result).toContain('/translation-review');
expect(result).toContain('/read-in');
});
it('ends with a newline', () => {
expect(renderTemplate('copilot', makeCtx())).toMatch(/\n$/);
});
@ -149,6 +156,13 @@ describe('renderTemplate — cursor', () => {
expect(result).toContain('middle-grade');
});
it('mentions shared workspace skills from the .claude folder', () => {
const result = renderTemplate('cursor', makeCtx());
expect(result).toContain('.claude/skills/');
expect(result).toContain('/translation-review');
expect(result).toContain('/proof-read');
});
it('ends with a newline', () => {
expect(renderTemplate('cursor', makeCtx())).toMatch(/\n$/);
});
@ -179,6 +193,13 @@ describe('renderTemplate — agents', () => {
expect(result).toContain('Arc/');
});
it('mentions shared workspace skills from the .claude folder', () => {
const result = renderTemplate('agents', makeCtx());
expect(result).toContain('.claude/skills/');
expect(result).toContain('/translation-review');
expect(result).toContain('/review');
});
it('ends with a newline', () => {
expect(renderTemplate('agents', makeCtx())).toMatch(/\n$/);
});
@ -213,6 +234,12 @@ describe('renderTemplate — review skill', () => {
expect(result).toContain('.bindery/settings.json');
});
it('offers translation-review when translated files changed', () => {
const result = renderTemplate('review', makeCtx());
expect(result).toContain('/translation-review');
expect(result).toContain('translated chapter files');
});
it('does not embed audience at generation time', () => {
const result = renderTemplate('review', makeMinimalCtx());
expect(result).not.toContain('middle-grade');
@ -273,6 +300,36 @@ describe('renderTemplate — translate skill', () => {
});
});
describe('renderTemplate — translation-review skill', () => {
it('contains YAML front-matter, title, trigger, tools, and handoff rules', () => {
const result = renderTemplate('translation-review', makeCtx());
expect(result).toContain('name: translation-review');
expect(result).toContain('# Skill: /translation-review');
expect(result).toContain('## Trigger');
expect(result).toContain('## Not this skill');
expect(result).toContain('## Tools');
expect(result).toContain('## Cross-skill handoff');
expect(result).toContain('## Rules');
});
it('references diff review, glossary loading, and source review handoff', () => {
const result = renderTemplate('translation-review', makeCtx());
expect(result).toContain('get_review_text');
expect(result).toContain('get_translation');
expect(result).toContain('/review');
expect(result).toContain('line parity');
});
it('uses agent-agnostic context wording and glossary persistence tools', () => {
const result = renderTemplate('translation-review', makeCtx());
expect(result).toContain('recent conversation');
expect(result).toContain('ask the user if ambiguous');
expect(result).toContain('search(query, targetLanguage)');
expect(result).toContain('add_translation');
expect(result).toContain('call `add_translation` before moving on');
});
});
describe('renderTemplate — status skill', () => {
it('contains YAML front-matter, title, trigger, tools, and steps', () => {
const result = renderTemplate('status', makeCtx());

View file

@ -343,6 +343,26 @@ describe('mcp tools', () => {
expect((settings as Record<string, unknown>)['bookTitle']).toBe('Test Book');
});
it('setup_ai_files accepts translation-review as a valid saved skill', () => {
const root = makeRoot();
write(path.join(root, '.bindery', 'settings.json'), JSON.stringify({
bookTitle: 'Test Book',
storyFolder: 'Story',
languages: [{ code: 'EN', folderName: 'EN' }, { code: 'NL', folderName: 'NL' }],
}, null, 2) + '\n');
toolSetupAiFiles(root, { targets: ['claude'], skills: ['translation-review'], overwrite: true });
const settings = JSON.parse(fs.readFileSync(path.join(root, '.bindery', 'settings.json'), 'utf-8')) as {
aiTargets?: string[];
aiSkills?: string[];
};
expect(settings.aiTargets).toEqual(['claude']);
expect(settings.aiSkills).toEqual(['translation-review']);
expect(fs.existsSync(path.join(root, '.claude', 'skills', 'translation-review', 'SKILL.md'))).toBe(true);
});
it('setup_ai_files does not set aiSkills when claude is not a target', () => {
const root = makeRoot();
write(path.join(root, '.bindery', 'settings.json'), JSON.stringify({

View file

@ -633,7 +633,7 @@
"items": {
"type": "string"
},
"description": "Claude skills to include: review, brainstorm, memory, translate, status, continuity, read-aloud, read-in, proof-read. Default: all."
"description": "Claude skills to include: review, brainstorm, memory, translate, translation-review, status, continuity, read-aloud, read-in, proof-read. Default: all."
},
"overwrite": {
"type": "boolean",

View file

@ -85,6 +85,7 @@ export type SkillTemplate =
| 'brainstorm'
| 'memory'
| 'translate'
| 'translation-review'
| 'status'
| 'continuity'
| 'read-aloud'
@ -92,7 +93,7 @@ export type SkillTemplate =
| 'proof-read';
export const ALL_SKILLS: SkillTemplate[] = [
'review', 'brainstorm', 'memory', 'translate', 'status', 'continuity', 'read-aloud', 'read-in', 'proof-read',
'review', 'brainstorm', 'memory', 'translate', 'translation-review', 'status', 'continuity', 'read-aloud', 'read-in', 'proof-read',
];
// ─── Entry point ──────────────────────────────────────────────────────────────

View file

@ -827,6 +827,7 @@ const SKILL_ITEMS: Array<{ label: string; description: string; value: SkillTempl
{ label: '/brainstorm', description: 'Generate plot / character / scene ideas', value: 'brainstorm' },
{ label: '/memory', description: 'Update memory files and compact if needed', value: 'memory' },
{ label: '/translate', description: 'Assisted chapter translation', value: 'translate' },
{ label: '/translation-review', description: 'Review hand-crafted translation against source text', value: 'translation-review' },
{ label: '/status', description: 'Book progress snapshot', value: 'status' },
{ label: '/continuity', description: 'Cross-check chapter for consistency errors', value: 'continuity' },
{ label: '/read-aloud', description: 'Reading-aloud test for a chapter or passage', value: 'read-aloud' },