From cbcd066588d6cb88abbd01cca179d720ecc2c8d5 Mon Sep 17 00:00:00 2001 From: Erik van der Boom Date: Sun, 26 Apr 2026 22:04:19 +0200 Subject: [PATCH] feat: add translation-review skill and update related documentation --- .github/copilot-instructions.md | 2 +- mcp-ts/src/aisetup.ts | 3 +- mcp-ts/src/index.ts | 2 +- mcp-ts/src/templates.ts | 104 +++++++++++++++++++++++++++++--- mcp-ts/test/aisetup.test.ts | 21 +++++++ mcp-ts/test/templates.test.ts | 57 +++++++++++++++++ mcp-ts/test/tools.test.ts | 20 ++++++ vscode-ext/package.json | 2 +- vscode-ext/src/ai-setup.ts | 3 +- vscode-ext/src/extension.ts | 1 + 10 files changed, 203 insertions(+), 12 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 251e053..37a2404 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -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. diff --git a/mcp-ts/src/aisetup.ts b/mcp-ts/src/aisetup.ts index f2f5c31..e13f3a3 100644 --- a/mcp-ts/src/aisetup.ts +++ b/mcp-ts/src/aisetup.ts @@ -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 { diff --git a/mcp-ts/src/index.ts b/mcp-ts/src/index.ts index 3f74ca6..0090f4c 100644 --- a/mcp-ts/src/index.ts +++ b/mcp-ts/src/index.ts @@ -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 }, diff --git a/mcp-ts/src/templates.ts b/mcp-ts/src/templates.ts index 4193d4b..d26d3a1 100644 --- a/mcp-ts/src/templates.ts +++ b/mcp-ts/src/templates.ts @@ -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 = { - '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 ` ${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 diff --git a/mcp-ts/test/aisetup.test.ts b/mcp-ts/test/aisetup.test.ts index 47ab117..68f42b5 100644 --- a/mcp-ts/test/aisetup.test.ts +++ b/mcp-ts/test/aisetup.test.ts @@ -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(); diff --git a/mcp-ts/test/templates.test.ts b/mcp-ts/test/templates.test.ts index aea7715..27462e4 100644 --- a/mcp-ts/test/templates.test.ts +++ b/mcp-ts/test/templates.test.ts @@ -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()); diff --git a/mcp-ts/test/tools.test.ts b/mcp-ts/test/tools.test.ts index fffdd1e..5ef7768 100644 --- a/mcp-ts/test/tools.test.ts +++ b/mcp-ts/test/tools.test.ts @@ -343,6 +343,26 @@ describe('mcp tools', () => { expect((settings as Record)['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({ diff --git a/vscode-ext/package.json b/vscode-ext/package.json index 5778df2..0a7b224 100644 --- a/vscode-ext/package.json +++ b/vscode-ext/package.json @@ -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", diff --git a/vscode-ext/src/ai-setup.ts b/vscode-ext/src/ai-setup.ts index eee04f8..92f0218 100644 --- a/vscode-ext/src/ai-setup.ts +++ b/vscode-ext/src/ai-setup.ts @@ -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 ────────────────────────────────────────────────────────────── diff --git a/vscode-ext/src/extension.ts b/vscode-ext/src/extension.ts index d0222ba..558a079 100644 --- a/vscode-ext/src/extension.ts +++ b/vscode-ext/src/extension.ts @@ -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' },