* fix(#2402): honor response_language across orchestrator output + UAT checkpoint renderer Replays the in-flight bot branch fix/2402-response-language-orchestrator-coverage (seven commits, never pushed) onto current origin/next as a single squashed commit. The original work was substantial and correct; this commit preserves its full scope, trimmed where rebase conflicts + workflow size budgets required it. Three independent layers where response_language was being dropped are closed: Layer 1 — orchestrator-facing directives across workflows. Adds the strong "All user-facing output in this workflow MUST be presented in {response_language}; technical terms, code, paths, and subagent prompts stay in English" directive to ~40 workflows that previously either lacked it entirely (verify-work, new-project, new-milestone, quick, manager, and ~35 more) or carried only the weak subagent-prompt-only form (plan-phase, execute-phase). The directive covers narration between tool calls and banner output, not just the AskUserQuestion prompts. Layer 2 — UAT checkpoint renderer (src/uat.cts). buildCheckpoint now accepts an optional responseLanguage parameter and renders the frame strings ("CHECKPOINT: Verification Required", "Type `pass` or describe what's wrong.") in any of 9 languages (English/Spanish/French/German/Portuguese/Japanese/ Chinese/Korean/Italian) with an alias table covering ~30 input variants (en, es, español, ja, 日本語, etc.). cmdRenderCheckpoint reads config.response_language via loadConfig(cwd) and passes it through, so the byte-for-byte block verify-work.md reprints verbatim is already localized when written — preserving the anti-injection hygiene rule at verify-work.md (the model is forbidden to translate after the fact). CJK display width is computed by East Asian Width property ranges (W/F) so the right ║ border of the banner stays aligned for full-width characters. English fallback is byte-identical to the pre-fix behavior when response_language is unset or unrecognized. Layer 3 — literal English report templates in execute-phase. The top-of- workflow directive covers all template sites (templates are a structural source, not literal output). Inline render-language notes that previously sat at each template site were removed during the squash because they pushed execute-phase.md over its frozen pre-phase-6 byte ceiling (93600 — ADR-857 Phase 6 capstone). The single top directive covers the same surface with fewer bytes. Also extends src/docs.cts and src/init.cts to propagate response_language into the init JSON bundle of the additional workflows so the directive can read it. Tests added: - tests/uat.test.cjs: buildCheckpoint with unset/unrecognized language falls back to English default; recognized language swaps only the two frame strings while structural lines stay untouched; CJK display-width regression (independent recomputation of East Asian Width W/F ranges). - tests/workspace.test.cjs, tests/docs-update.test.cjs: response_language wiring through docs.cts/init.cts. References: #2402; reporter's three-layer triage + Layer-4 follow-up; the byte-for-byte anti-injection hygiene rule at verify-work.md (the reason Layer 2 must be renderer-side, not model-translated). This is a squash of the in-flight bot branch — seven commits representing the original implementation plus its subsequent fix/CJK-padding/test/ changeset/regen cycles, none of which were ever pushed or PR'd. The squash captures the final coherent state. * chore(#2402): backfill pr:2457 in .changeset/2402-response-language-orchestrator-coverage.md * chore(#2402): regen golden + size baseline after rebase against #2315 (PR #2451) Rebase conflicts were entirely in generated artifacts (golden-install-parity fixtures + workflow-size-baseline.json). After taking theirs during rebase, regenerated cleanly against the merged source tree.
This commit is contained in:
@@ -285,6 +285,12 @@ function cmdDocsInit(cwd: string, raw: boolean): void {
|
||||
const agentStatus = checkAgentsInstalled();
|
||||
result['agents_installed'] = agentStatus.agents_installed;
|
||||
result['missing_agents'] = agentStatus.missing_agents;
|
||||
// #2402: withProjectRoot injects response_language when set; cmdDocsInit predates
|
||||
// that helper and never picked it up, so docs-update's orchestrator-owned prompts
|
||||
// silently stayed English even with response_language configured.
|
||||
if (config.response_language) {
|
||||
result['response_language'] = config.response_language;
|
||||
}
|
||||
output(result, raw, undefined);
|
||||
}
|
||||
|
||||
|
||||
@@ -2092,7 +2092,9 @@ function cmdInitRemoveWorkspace(cwd: string, name: string | undefined, raw: bool
|
||||
has_dirty_repos: dirtyRepos.length > 0,
|
||||
};
|
||||
|
||||
output(result, raw);
|
||||
// #2402: sibling init commands route through withProjectRoot so response_language
|
||||
// (and project_root/agents_installed) reach the workflow; this one didn't.
|
||||
output(withProjectRoot(cwd, result), raw);
|
||||
}
|
||||
|
||||
function buildAgentSkillsBlock(
|
||||
|
||||
132
src/uat.cts
132
src/uat.cts
@@ -36,6 +36,9 @@ const { extractFrontmatter } = frontmatter;
|
||||
import phaseIdMod = require('./phase-id.cjs');
|
||||
const { PHASE_NUMBER_TOKEN_SOURCE } = phaseIdMod;
|
||||
import { requireSafePath, sanitizeForDisplay } from './security.cjs';
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports -- config-loader.cjs is an export= CommonJS module
|
||||
import configLoader = require('./config-loader.cjs');
|
||||
const { loadConfig } = configLoader;
|
||||
|
||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -200,7 +203,9 @@ function cmdRenderCheckpoint(cwd: string, options: { file?: string } = {}, raw:
|
||||
error('UAT session is already complete; no pending checkpoint to render');
|
||||
}
|
||||
|
||||
const checkpoint = buildCheckpoint(currentTest as Required<Omit<CurrentTest, 'complete'>> & { complete: false });
|
||||
const config = loadConfig(cwd);
|
||||
const responseLanguage = typeof config.response_language === 'string' ? config.response_language : undefined;
|
||||
const checkpoint = buildCheckpoint(currentTest as Required<Omit<CurrentTest, 'complete'>> & { complete: false }, responseLanguage);
|
||||
output({
|
||||
file_path: toPosixPath(path.relative(cwd, resolvedPath)),
|
||||
test_number: currentTest.number,
|
||||
@@ -342,11 +347,130 @@ function parseExpectedFromTestBlock(block: string): string | null {
|
||||
}
|
||||
|
||||
// ─── buildCheckpoint ──────────────────────────────────────────────────────────
|
||||
//
|
||||
// Localized frame strings (#2402): the checkpoint banner + instruction line are
|
||||
// the byte-for-byte block verify-work.md reprints verbatim, so the model can't
|
||||
// translate it after the fact — the frame must already be in `response_language`
|
||||
// when this function returns it. Bounded table with an ENGLISH FALLBACK for
|
||||
// unset/unrecognized languages keeps the default path byte-identical.
|
||||
|
||||
function buildCheckpoint(currentTest: { number: number; name: string; expected: string }): string {
|
||||
interface CheckpointFrame {
|
||||
banner: string;
|
||||
instruction: string;
|
||||
}
|
||||
|
||||
const CHECKPOINT_BOX_WIDTH = 64; // total column width of the ╔══...╗ border, borders stay byte-identical
|
||||
|
||||
const CHECKPOINT_FRAMES: Record<string, CheckpointFrame> = {
|
||||
english: {
|
||||
banner: 'CHECKPOINT: Verification Required',
|
||||
instruction: 'Type `pass` or describe what\'s wrong.',
|
||||
},
|
||||
spanish: {
|
||||
banner: 'PUNTO DE CONTROL: Verificación requerida',
|
||||
instruction: 'Escribe `pass` o describe qué está mal.',
|
||||
},
|
||||
french: {
|
||||
banner: 'POINT DE CONTRÔLE : Vérification requise',
|
||||
instruction: 'Tapez `pass` ou décrivez ce qui ne va pas.',
|
||||
},
|
||||
german: {
|
||||
banner: 'KONTROLLPUNKT: Überprüfung erforderlich',
|
||||
instruction: 'Gib `pass` ein oder beschreibe, was nicht stimmt.',
|
||||
},
|
||||
portuguese: {
|
||||
banner: 'PONTO DE VERIFICAÇÃO: Verificação necessária',
|
||||
instruction: 'Digite `pass` ou descreva o que está errado.',
|
||||
},
|
||||
japanese: {
|
||||
banner: 'チェックポイント: 検証が必要です',
|
||||
instruction: '`pass` と入力するか、問題点を説明してください。',
|
||||
},
|
||||
chinese: {
|
||||
banner: '检查点:需要验证',
|
||||
instruction: '输入 `pass` 或描述问题所在。',
|
||||
},
|
||||
korean: {
|
||||
banner: '체크포인트: 검증 필요',
|
||||
instruction: '`pass`를 입력하거나 문제를 설명하세요.',
|
||||
},
|
||||
italian: {
|
||||
banner: 'PUNTO DI CONTROLLO: Verifica richiesta',
|
||||
instruction: 'Digita `pass` o descrivi cosa non va.',
|
||||
},
|
||||
};
|
||||
|
||||
// Free-form response_language aliases → canonical CHECKPOINT_FRAMES key.
|
||||
const CHECKPOINT_LANGUAGE_ALIASES: Record<string, string> = {
|
||||
english: 'english', en: 'english', 'en-us': 'english', 'en-gb': 'english',
|
||||
spanish: 'spanish', es: 'spanish', 'español': 'spanish', espanol: 'spanish', castellano: 'spanish',
|
||||
french: 'french', fr: 'french', 'français': 'french', francais: 'french',
|
||||
german: 'german', de: 'german', deutsch: 'german',
|
||||
portuguese: 'portuguese', pt: 'portuguese', 'pt-br': 'portuguese', 'português': 'portuguese', portugues: 'portuguese', 'brazilian portuguese': 'portuguese',
|
||||
japanese: 'japanese', ja: 'japanese', '日本語': 'japanese',
|
||||
chinese: 'chinese', zh: 'chinese', 'zh-cn': 'chinese', 'zh-tw': 'chinese', mandarin: 'chinese', 'simplified chinese': 'chinese', 'traditional chinese': 'chinese', '中文': 'chinese',
|
||||
korean: 'korean', ko: 'korean', '한국어': 'korean',
|
||||
italian: 'italian', it: 'italian', italiano: 'italian',
|
||||
};
|
||||
|
||||
function resolveCheckpointFrame(responseLanguage: string | undefined): CheckpointFrame {
|
||||
if (!responseLanguage) return CHECKPOINT_FRAMES.english;
|
||||
const key = CHECKPOINT_LANGUAGE_ALIASES[responseLanguage.trim().toLowerCase()];
|
||||
return (key && CHECKPOINT_FRAMES[key]) || CHECKPOINT_FRAMES.english;
|
||||
}
|
||||
|
||||
// Approximate East Asian Width ranges (Unicode property values W and F) — the
|
||||
// CJK scripts CHECKPOINT_FRAMES ships (Japanese/Chinese/Korean) render each
|
||||
// matching code point at 2 terminal/display columns, not 1. Padding computed
|
||||
// from `.length` (UTF-16 code units) undercounts these by one column per
|
||||
// wide character, visually misaligning the box's right border (#2402 review
|
||||
// medium finding). Latin-script frames (English/Spanish/French/German/
|
||||
// Portuguese/Italian) contain no wide code points, so displayWidth === length
|
||||
// for them — no behavior change there.
|
||||
function isWideCodePoint(codePoint: number): boolean {
|
||||
return (
|
||||
(codePoint >= 0x1100 && codePoint <= 0x115f) || // Hangul Jamo
|
||||
codePoint === 0x2329 || codePoint === 0x232a ||
|
||||
(codePoint >= 0x2e80 && codePoint <= 0x303e) || // CJK Radicals .. CJK Symbols and Punctuation
|
||||
(codePoint >= 0x3041 && codePoint <= 0x33ff) || // Hiragana .. CJK Compatibility
|
||||
(codePoint >= 0x3400 && codePoint <= 0x4dbf) || // CJK Unified Ideographs Extension A
|
||||
(codePoint >= 0x4e00 && codePoint <= 0x9fff) || // CJK Unified Ideographs
|
||||
(codePoint >= 0xa000 && codePoint <= 0xa4cf) || // Yi Syllables
|
||||
(codePoint >= 0xac00 && codePoint <= 0xd7a3) || // Hangul Syllables
|
||||
(codePoint >= 0xf900 && codePoint <= 0xfaff) || // CJK Compatibility Ideographs
|
||||
(codePoint >= 0xfe30 && codePoint <= 0xfe4f) || // CJK Compatibility Forms
|
||||
(codePoint >= 0xff00 && codePoint <= 0xff60) || // Fullwidth Forms
|
||||
(codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
|
||||
(codePoint >= 0x20000 && codePoint <= 0x3fffd) // CJK Unified Ideographs Extension B+ / supplementary
|
||||
);
|
||||
}
|
||||
|
||||
// Iterates by Unicode code point (not UTF-16 code unit) so astral characters
|
||||
// are measured once, not as two surrogate units.
|
||||
function displayWidth(text: string): number {
|
||||
let width = 0;
|
||||
for (const ch of text) {
|
||||
width += isWideCodePoint(ch.codePointAt(0) as number) ? 2 : 1;
|
||||
}
|
||||
return width;
|
||||
}
|
||||
|
||||
// Pads `text` into a `║ text… ║` line matching CHECKPOINT_BOX_WIDTH. Content
|
||||
// that overflows the box (a longer translated string) is left unpadded rather
|
||||
// than truncated — a slightly ragged border beats losing text.
|
||||
function checkpointBoxLine(text: string): string {
|
||||
const innerWidth = CHECKPOINT_BOX_WIDTH - 2;
|
||||
const content = ` ${text}`;
|
||||
const padLength = innerWidth - displayWidth(content);
|
||||
const padded = padLength > 0 ? content + ' '.repeat(padLength) : content;
|
||||
return `║${padded}║`;
|
||||
}
|
||||
|
||||
function buildCheckpoint(currentTest: { number: number; name: string; expected: string }, responseLanguage?: string): string {
|
||||
const frame = resolveCheckpointFrame(responseLanguage);
|
||||
return [
|
||||
'╔══════════════════════════════════════════════════════════════╗',
|
||||
'║ CHECKPOINT: Verification Required ║',
|
||||
checkpointBoxLine(frame.banner),
|
||||
'╚══════════════════════════════════════════════════════════════╝',
|
||||
'',
|
||||
`**Test ${currentTest.number}: ${currentTest.name}**`,
|
||||
@@ -354,7 +478,7 @@ function buildCheckpoint(currentTest: { number: number; name: string; expected:
|
||||
currentTest.expected,
|
||||
'',
|
||||
'──────────────────────────────────────────────────────────────',
|
||||
'Type `pass` or describe what\'s wrong.',
|
||||
frame.instruction,
|
||||
'──────────────────────────────────────────────────────────────',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user