mirror of
https://github.com/evdboom/Bindery.git
synced 2026-07-22 06:49:36 +00:00
feat: add translation-review skill and update related documentation
This commit is contained in:
parent
d4f8edc6d9
commit
cbcd066588
10 changed files with 203 additions and 12 deletions
2
.github/copilot-instructions.md
vendored
2
.github/copilot-instructions.md
vendored
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 ──────────────────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
|
|
|
|||
Loading…
Reference in a new issue