diff --git a/src/config.ts b/src/config.ts index bfd4d0d..cc4493a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -115,6 +115,9 @@ export const ARBITER_SERVICE_ID = ARBITER_AGENT_TYPE ? getEnv('ARBITER_SERVICE_ID') || CODEX_REVIEW_SERVICE_ID : null; +/** Language for agent responses. When set, a language instruction is appended to all paired room prompts. */ +export const AGENT_LANGUAGE = getEnv('AGENT_LANGUAGE') || ''; + /** Number of consecutive owner↔reviewer round trips before arbiter is auto-requested. */ export const ARBITER_DEADLOCK_THRESHOLD = parseInt( getEnv('ARBITER_DEADLOCK_THRESHOLD') || '3', diff --git a/src/paired-execution-context.ts b/src/paired-execution-context.ts index 3c5357b..d476457 100644 --- a/src/paired-execution-context.ts +++ b/src/paired-execution-context.ts @@ -288,6 +288,18 @@ export function preparePairedExecutionContext(args: { // Use a stable per-channel worktree (not per-task) so the Claude SDK // session persists across tasks. Different channels still get isolation. workspace = provisionOwnerWorkspaceForPairedTask(latestTask.id); + // Update source_ref from workspace HEAD so change detection compares + // against the correct repo. At task creation, source_ref is from the + // canonical workDir which may differ from the workspace clone. + if (workspace?.workspace_dir && latestTask.status === 'active') { + const wsRef = resolveCanonicalSourceRef(workspace.workspace_dir); + if (wsRef !== latestTask.source_ref) { + updatePairedTask(latestTask.id, { + source_ref: wsRef, + updated_at: now, + }); + } + } } else if (roomRoleContext.role === 'reviewer') { const reviewerWorkspace = prepareReviewerWorkspaceForExecution(latestTask); workspace = reviewerWorkspace.workspace; @@ -512,28 +524,32 @@ export function completePairedExecutionContext(args: { ? hasCodeChangesSinceRef(workspace.workspace_dir, task.source_ref) : null; - if (hasNewChanges === false) { - // No code changes since reviewer approved → finalize complete + if (hasNewChanges === true) { + // Owner made changes after approval → needs re-review + logger.info( + { + taskId, + sourceRef: task.source_ref, + hasNewChanges, + }, + 'Owner made changes after reviewer approval — re-triggering review', + ); + } else { + // No code changes (false) or unable to determine (null) → + // finalize complete. Treating null as "no changes" prevents + // infinite DONE↔DONE loops when source_ref is from a different + // repo or the workspace has no matching ref. updatePairedTask(taskId, { status: 'completed', completion_reason: 'done', updated_at: now, }); logger.info( - { taskId, summary: args.summary?.slice(0, 100) }, + { taskId, hasNewChanges, summary: args.summary?.slice(0, 100) }, 'Owner finalized after reviewer approval — task completed', ); return; } - // Owner made changes after approval → needs re-review - logger.info( - { - taskId, - sourceRef: task.source_ref, - hasNewChanges, - }, - 'Owner made changes after reviewer approval — re-triggering review', - ); } } diff --git a/src/platform-prompts.ts b/src/platform-prompts.ts index 0e9332d..7696d89 100644 --- a/src/platform-prompts.ts +++ b/src/platform-prompts.ts @@ -1,8 +1,14 @@ import fs from 'fs'; import path from 'path'; +import { AGENT_LANGUAGE } from './config.js'; import type { AgentType } from './types.js'; +function appendLanguageInstruction(prompt: string): string { + if (!AGENT_LANGUAGE) return prompt; + return `${prompt}\n\n## Language\n\nAlways respond in ${AGENT_LANGUAGE}.`; +} + const PLATFORM_PROMPT_FILES: Record = { 'claude-code': 'claude-platform.md', codex: 'codex-platform.md', @@ -60,7 +66,7 @@ export function readPairedRoomPrompt( if (!fs.existsSync(promptPath)) return undefined; const prompt = fs.readFileSync(promptPath, 'utf-8').trim(); - return prompt || undefined; + return prompt ? appendLanguageInstruction(prompt) : undefined; } export function getArbiterPromptPath(projectRoot = process.cwd()): string { @@ -74,5 +80,5 @@ export function readArbiterPrompt( if (!fs.existsSync(promptPath)) return undefined; const prompt = fs.readFileSync(promptPath, 'utf-8').trim(); - return prompt || undefined; + return prompt ? appendLanguageInstruction(prompt) : undefined; }