From dc2afa299bccd80c88b99a3e8d25b9b799a35e80 Mon Sep 17 00:00:00 2001 From: Tibsfox Date: Sat, 4 Apr 2026 04:49:10 -0700 Subject: [PATCH] feat(i18n): add response_language config for cross-phase language consistency (#1412) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(i18n): add response_language config for cross-phase language consistency Adds `response_language` config key that propagates through all init outputs via withProjectRoot(). Workflows read this field and instruct agents to present user-facing questions in the configured language, solving the problem of language preference resetting at phase boundaries. Usage: gsd-tools config-set response_language "Portuguese" Closes #1399 Co-Authored-By: Claude Opus 4.6 (1M context) * test(security): allowlist discuss-phase.md for size threshold discuss-phase.md legitimately exceeds 50K chars due to power mode and i18n directives — not prompt stuffing. Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: Claude Opus 4.6 (1M context) --- get-shit-done/bin/lib/config.cjs | 1 + get-shit-done/bin/lib/core.cjs | 1 + get-shit-done/bin/lib/init.cjs | 7 ++++++ get-shit-done/references/planning-config.md | 1 + get-shit-done/workflows/discuss-phase.md | 4 +++- get-shit-done/workflows/execute-phase.md | 4 +++- get-shit-done/workflows/plan-phase.md | 4 +++- tests/core.test.cjs | 12 ++++++++++ tests/init-manager.test.cjs | 25 +++++++++++++++++++++ tests/prompt-injection-scan.test.cjs | 2 ++ 10 files changed, 58 insertions(+), 3 deletions(-) diff --git a/get-shit-done/bin/lib/config.cjs b/get-shit-done/bin/lib/config.cjs index ab09c8f23..db3e4e80d 100644 --- a/get-shit-done/bin/lib/config.cjs +++ b/get-shit-done/bin/lib/config.cjs @@ -29,6 +29,7 @@ const VALID_CONFIG_KEYS = new Set([ 'hooks.context_warnings', 'project_code', 'phase_naming', 'manager.flags.discuss', 'manager.flags.plan', 'manager.flags.execute', + 'response_language', ]); /** diff --git a/get-shit-done/bin/lib/core.cjs b/get-shit-done/bin/lib/core.cjs index 476b85da2..20a6ab4b5 100644 --- a/get-shit-done/bin/lib/core.cjs +++ b/get-shit-done/bin/lib/core.cjs @@ -357,6 +357,7 @@ function loadConfig(cwd) { model_overrides: parsed.model_overrides || null, agent_skills: parsed.agent_skills || {}, manager: parsed.manager || {}, + response_language: get('response_language') || null, }; } catch { return defaults; diff --git a/get-shit-done/bin/lib/init.cjs b/get-shit-done/bin/lib/init.cjs index edf8dee4b..55eafc375 100644 --- a/get-shit-done/bin/lib/init.cjs +++ b/get-shit-done/bin/lib/init.cjs @@ -37,6 +37,13 @@ function withProjectRoot(cwd, result) { const agentStatus = checkAgentsInstalled(); result.agents_installed = agentStatus.agents_installed; result.missing_agents = agentStatus.missing_agents; + // Inject response_language into all init outputs (#1399). + // Workflows propagate this to subagent prompts so user-facing questions + // stay in the configured language across phase boundaries. + const config = loadConfig(cwd); + if (config.response_language) { + result.response_language = config.response_language; + } return result; } diff --git a/get-shit-done/references/planning-config.md b/get-shit-done/references/planning-config.md index aba562b98..de7dcb6b3 100644 --- a/get-shit-done/references/planning-config.md +++ b/get-shit-done/references/planning-config.md @@ -38,6 +38,7 @@ Configuration options for `.planning/` directory behavior. | `manager.flags.discuss` | `""` | Flags passed to `/gsd:discuss-phase` when dispatched from manager (e.g. `"--auto --analyze"`) | | `manager.flags.plan` | `""` | Flags passed to plan workflow when dispatched from manager | | `manager.flags.execute` | `""` | Flags passed to execute workflow when dispatched from manager | +| `response_language` | `null` | Language for user-facing questions and prompts across all phases/subagents (e.g. `"Portuguese"`, `"Japanese"`, `"Spanish"`). When set, all spawned agents include a directive to respond in this language. | diff --git a/get-shit-done/workflows/discuss-phase.md b/get-shit-done/workflows/discuss-phase.md index 519367629..69a2d92c2 100644 --- a/get-shit-done/workflows/discuss-phase.md +++ b/get-shit-done/workflows/discuss-phase.md @@ -137,7 +137,9 @@ if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi AGENT_SKILLS_ADVISOR=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" agent-skills gsd-advisor 2>/dev/null) ``` -Parse JSON for: `commit_docs`, `phase_found`, `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `padded_phase`, `has_research`, `has_context`, `has_plans`, `has_verification`, `plan_count`, `roadmap_exists`, `planning_exists`. +Parse JSON for: `commit_docs`, `phase_found`, `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `padded_phase`, `has_research`, `has_context`, `has_plans`, `has_verification`, `plan_count`, `roadmap_exists`, `planning_exists`, `response_language`. + +**If `response_language` is set:** All user-facing questions, prompts, and explanations in this workflow MUST be presented in `{response_language}`. This includes AskUserQuestion labels, option text, gray area descriptions, and discussion summaries. Technical terms, code, and file paths remain in English. Subagent prompts stay in English — only user-facing output is translated. **If `phase_found` is false:** ``` diff --git a/get-shit-done/workflows/execute-phase.md b/get-shit-done/workflows/execute-phase.md index c8a841ed8..90f449c8e 100644 --- a/get-shit-done/workflows/execute-phase.md +++ b/get-shit-done/workflows/execute-phase.md @@ -66,7 +66,9 @@ if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi AGENT_SKILLS=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" agent-skills gsd-executor 2>/dev/null) ``` -Parse JSON for: `executor_model`, `verifier_model`, `commit_docs`, `parallelization`, `branching_strategy`, `branch_name`, `phase_found`, `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `plans`, `incomplete_plans`, `plan_count`, `incomplete_count`, `state_exists`, `roadmap_exists`, `phase_req_ids`. +Parse JSON for: `executor_model`, `verifier_model`, `commit_docs`, `parallelization`, `branching_strategy`, `branch_name`, `phase_found`, `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `plans`, `incomplete_plans`, `plan_count`, `incomplete_count`, `state_exists`, `roadmap_exists`, `phase_req_ids`, `response_language`. + +**If `response_language` is set:** Include `response_language: {value}` in all spawned subagent prompts so any user-facing output stays in the configured language. Read worktree config: diff --git a/get-shit-done/workflows/plan-phase.md b/get-shit-done/workflows/plan-phase.md index 630ab50a9..335e1d7f7 100644 --- a/get-shit-done/workflows/plan-phase.md +++ b/get-shit-done/workflows/plan-phase.md @@ -32,7 +32,9 @@ CONTEXT_WINDOW=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-get When `CONTEXT_WINDOW >= 500000`, the planner prompt includes prior phase CONTEXT.md files so cross-phase decisions are consistent (e.g., "use library X for all data fetching" from Phase 2 is visible to Phase 5's planner). -Parse JSON for: `researcher_model`, `planner_model`, `checker_model`, `research_enabled`, `plan_checker_enabled`, `nyquist_validation_enabled`, `commit_docs`, `text_mode`, `phase_found`, `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `padded_phase`, `has_research`, `has_context`, `has_reviews`, `has_plans`, `plan_count`, `planning_exists`, `roadmap_exists`, `phase_req_ids`. +Parse JSON for: `researcher_model`, `planner_model`, `checker_model`, `research_enabled`, `plan_checker_enabled`, `nyquist_validation_enabled`, `commit_docs`, `text_mode`, `phase_found`, `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `padded_phase`, `has_research`, `has_context`, `has_reviews`, `has_plans`, `plan_count`, `planning_exists`, `roadmap_exists`, `phase_req_ids`, `response_language`. + +**If `response_language` is set:** Include `response_language: {value}` in all spawned subagent prompts so any user-facing output stays in the configured language. **File paths (for blocks):** `state_path`, `roadmap_path`, `requirements_path`, `context_path`, `research_path`, `verification_path`, `uat_path`, `reviews_path`. These are null if files don't exist. diff --git a/tests/core.test.cjs b/tests/core.test.cjs index 61dfad807..b35121d50 100644 --- a/tests/core.test.cjs +++ b/tests/core.test.cjs @@ -99,6 +99,18 @@ describe('loadConfig', () => { assert.strictEqual(config.model_overrides, null); }); + test('reads response_language when set', () => { + writeConfig({ response_language: 'Portuguese' }); + const config = loadConfig(tmpDir); + assert.strictEqual(config.response_language, 'Portuguese'); + }); + + test('returns response_language as null when not set', () => { + writeConfig({ model_profile: 'balanced' }); + const config = loadConfig(tmpDir); + assert.strictEqual(config.response_language, null); + }); + test('returns defaults when config.json contains invalid JSON', () => { fs.writeFileSync( path.join(tmpDir, '.planning', 'config.json'), diff --git a/tests/init-manager.test.cjs b/tests/init-manager.test.cjs index 02a86eceb..14f22f9e8 100644 --- a/tests/init-manager.test.cjs +++ b/tests/init-manager.test.cjs @@ -506,4 +506,29 @@ describe('init manager', () => { const activeRecs = recommended.filter(r => r.phase === '1'); assert.strictEqual(activeRecs.length, 1, 'phase 1 should still be recommended'); }); + + test('output includes response_language when configured', () => { + writeState(tmpDir); + writeRoadmap(tmpDir, [{ number: '1', name: 'Test' }]); + + fs.writeFileSync( + path.join(tmpDir, '.planning', 'config.json'), + JSON.stringify({ response_language: 'Japanese' }) + ); + + const result = runGsdTools('init manager', tmpDir); + const output = JSON.parse(result.output); + + assert.strictEqual(output.response_language, 'Japanese'); + }); + + test('output omits response_language when not configured', () => { + writeState(tmpDir); + writeRoadmap(tmpDir, [{ number: '1', name: 'Test' }]); + + const result = runGsdTools('init manager', tmpDir); + const output = JSON.parse(result.output); + + assert.strictEqual(output.response_language, undefined); + }); }); diff --git a/tests/prompt-injection-scan.test.cjs b/tests/prompt-injection-scan.test.cjs index ad1cb8c02..17a34d683 100644 --- a/tests/prompt-injection-scan.test.cjs +++ b/tests/prompt-injection-scan.test.cjs @@ -48,8 +48,10 @@ const SCAN_DIRS = [ const SCAN_EXTS = new Set(['.md', '.cjs', '.js', '.json']); // Files that legitimately reference injection patterns (e.g., security docs, this test) +// or exceed the 50K size threshold due to legitimate workflow complexity const ALLOWLIST = new Set([ 'get-shit-done/bin/lib/security.cjs', // The security module itself + 'get-shit-done/workflows/discuss-phase.md', // Large workflow (~50K) with power mode + i18n 'hooks/gsd-prompt-guard.js', // The prompt guard hook 'tests/security.test.cjs', // Security tests 'tests/prompt-injection-scan.test.cjs', // This file