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:
@@ -115,6 +115,9 @@ export const ARBITER_SERVICE_ID = ARBITER_AGENT_TYPE
|
|||||||
? getEnv('ARBITER_SERVICE_ID') || CODEX_REVIEW_SERVICE_ID
|
? getEnv('ARBITER_SERVICE_ID') || CODEX_REVIEW_SERVICE_ID
|
||||||
: null;
|
: 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. */
|
/** Number of consecutive owner↔reviewer round trips before arbiter is auto-requested. */
|
||||||
export const ARBITER_DEADLOCK_THRESHOLD = parseInt(
|
export const ARBITER_DEADLOCK_THRESHOLD = parseInt(
|
||||||
getEnv('ARBITER_DEADLOCK_THRESHOLD') || '3',
|
getEnv('ARBITER_DEADLOCK_THRESHOLD') || '3',
|
||||||
|
|||||||
@@ -288,6 +288,18 @@ export function preparePairedExecutionContext(args: {
|
|||||||
// Use a stable per-channel worktree (not per-task) so the Claude SDK
|
// Use a stable per-channel worktree (not per-task) so the Claude SDK
|
||||||
// session persists across tasks. Different channels still get isolation.
|
// session persists across tasks. Different channels still get isolation.
|
||||||
workspace = provisionOwnerWorkspaceForPairedTask(latestTask.id);
|
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') {
|
} else if (roomRoleContext.role === 'reviewer') {
|
||||||
const reviewerWorkspace = prepareReviewerWorkspaceForExecution(latestTask);
|
const reviewerWorkspace = prepareReviewerWorkspaceForExecution(latestTask);
|
||||||
workspace = reviewerWorkspace.workspace;
|
workspace = reviewerWorkspace.workspace;
|
||||||
@@ -512,28 +524,32 @@ export function completePairedExecutionContext(args: {
|
|||||||
? hasCodeChangesSinceRef(workspace.workspace_dir, task.source_ref)
|
? hasCodeChangesSinceRef(workspace.workspace_dir, task.source_ref)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (hasNewChanges === false) {
|
if (hasNewChanges === true) {
|
||||||
// No code changes since reviewer approved → finalize complete
|
// 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, {
|
updatePairedTask(taskId, {
|
||||||
status: 'completed',
|
status: 'completed',
|
||||||
completion_reason: 'done',
|
completion_reason: 'done',
|
||||||
updated_at: now,
|
updated_at: now,
|
||||||
});
|
});
|
||||||
logger.info(
|
logger.info(
|
||||||
{ taskId, summary: args.summary?.slice(0, 100) },
|
{ taskId, hasNewChanges, summary: args.summary?.slice(0, 100) },
|
||||||
'Owner finalized after reviewer approval — task completed',
|
'Owner finalized after reviewer approval — task completed',
|
||||||
);
|
);
|
||||||
return;
|
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',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
|
import { AGENT_LANGUAGE } from './config.js';
|
||||||
import type { AgentType } from './types.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> = {
|
const PLATFORM_PROMPT_FILES: Record<AgentType, string> = {
|
||||||
'claude-code': 'claude-platform.md',
|
'claude-code': 'claude-platform.md',
|
||||||
codex: 'codex-platform.md',
|
codex: 'codex-platform.md',
|
||||||
@@ -60,7 +66,7 @@ export function readPairedRoomPrompt(
|
|||||||
if (!fs.existsSync(promptPath)) return undefined;
|
if (!fs.existsSync(promptPath)) return undefined;
|
||||||
|
|
||||||
const prompt = fs.readFileSync(promptPath, 'utf-8').trim();
|
const prompt = fs.readFileSync(promptPath, 'utf-8').trim();
|
||||||
return prompt || undefined;
|
return prompt ? appendLanguageInstruction(prompt) : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getArbiterPromptPath(projectRoot = process.cwd()): string {
|
export function getArbiterPromptPath(projectRoot = process.cwd()): string {
|
||||||
@@ -74,5 +80,5 @@ export function readArbiterPrompt(
|
|||||||
if (!fs.existsSync(promptPath)) return undefined;
|
if (!fs.existsSync(promptPath)) return undefined;
|
||||||
|
|
||||||
const prompt = fs.readFileSync(promptPath, 'utf-8').trim();
|
const prompt = fs.readFileSync(promptPath, 'utf-8').trim();
|
||||||
return prompt || undefined;
|
return prompt ? appendLanguageInstruction(prompt) : undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user