fix: prevent DONE↔DONE infinite loop and add AGENT_LANGUAGE setting

- Fix source_ref mismatch: update to workspace HEAD on first owner run
  so change detection compares against the correct repo
- Treat hasNewChanges === null as "no changes" at finalize to prevent
  infinite re-review when source_ref is unresolvable
- Add AGENT_LANGUAGE env var: when set, appends language instruction to
  all paired room prompts (owner, reviewer, arbiter)
This commit is contained in:
Eyejoker
2026-03-31 13:55:41 +09:00
parent 47d532e869
commit 757e33c58e
3 changed files with 39 additions and 14 deletions

View File

@@ -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',

View File

@@ -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 approvedfinalize complete
if (hasNewChanges === true) {
// Owner made changes after approvalneeds 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',
);
}
}

View File

@@ -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<AgentType, string> = {
'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;
}