refactor: share owner prompts and tighten silent output guidance

This commit is contained in:
Eyejoker
2026-03-28 07:46:13 +09:00
parent b97d1a6260
commit 772fea45df
13 changed files with 228 additions and 155 deletions

View File

@@ -222,6 +222,14 @@ describe('prepareGroupEnvironment codex auth handling', () => {
path.join(promptsDir, 'codex-review-paired-room.md'),
'review paired prompt\n',
);
fs.writeFileSync(
path.join(promptsDir, 'owner-common-platform.md'),
'owner common platform prompt\n',
);
fs.writeFileSync(
path.join(promptsDir, 'owner-common-paired-room.md'),
'owner common paired prompt\n',
);
fs.writeFileSync(
path.join(promptsDir, 'codex-review-failover-platform.md'),
'failover platform prompt\n',
@@ -253,12 +261,59 @@ describe('prepareGroupEnvironment codex auth handling', () => {
const segments = agents.trim().split('\n\n---\n\n');
expect(segments).toEqual([
'owner common platform prompt',
'failover platform prompt',
'owner common paired prompt',
'failover paired prompt',
'memory briefing',
]);
});
it('adds the shared owner prompt fragments to Claude session prompts', () => {
vi.mocked(db.isPairedRoomJid).mockReturnValue(true);
mockReadEnvFile.mockReturnValue({});
const promptsDir = path.join(tempRoot, 'prompts');
fs.mkdirSync(promptsDir, { recursive: true });
fs.writeFileSync(
path.join(promptsDir, 'owner-common-platform.md'),
'owner common platform prompt\n',
);
fs.writeFileSync(
path.join(promptsDir, 'owner-common-paired-room.md'),
'owner common paired prompt\n',
);
prepareGroupEnvironment(
{ ...group, agentType: 'claude-code' },
false,
'dc:test',
{
memoryBriefing: 'memory briefing',
},
);
const claudePath = path.join(
tempRoot,
'sessions',
group.folder,
'services',
'codex-main',
'.claude',
'CLAUDE.md',
);
const claude = fs.readFileSync(claudePath, 'utf-8');
const segments = claude.trim().split('\n\n---\n\n');
expect(segments).toEqual([
'owner common platform prompt',
'platform prompt',
'owner common paired prompt',
'paired room prompt',
'memory briefing',
]);
});
it('returns to the normal review prompt stack after failover is cleared', () => {
vi.mocked(config.isReviewService).mockReturnValue(true);
vi.mocked(db.isPairedRoomJid).mockReturnValue(true);

View File

@@ -258,10 +258,17 @@ function prepareCodexSessionEnvironment(args: {
const sessionAgents = (
args.useFailoverPromptPack
? [
readOptionalPromptFile(args.projectRoot, 'owner-common-platform.md'),
readOptionalPromptFile(
args.projectRoot,
'codex-review-failover-platform.md',
),
args.isPairedRoom
? readOptionalPromptFile(
args.projectRoot,
'owner-common-paired-room.md',
)
: undefined,
args.isPairedRoom
? readOptionalPromptFile(
args.projectRoot,
@@ -427,7 +434,14 @@ export function prepareGroupEnvironment(
effectiveLease.explicit &&
effectiveLease.owner_service_id === CODEX_REVIEW_SERVICE_ID;
const ownerCommonPlatformPrompt = readOptionalPromptFile(
projectRoot,
'owner-common-platform.md',
);
const claudePlatformPrompt = readPlatformPrompt('claude-code', projectRoot);
const ownerCommonPairedRoomPrompt = isPairedRoom
? readOptionalPromptFile(projectRoot, 'owner-common-paired-room.md')
: undefined;
const claudePairedRoomPrompt = isPairedRoom
? readPairedRoomPrompt('claude-code', projectRoot)
: undefined;
@@ -436,7 +450,9 @@ export function prepareGroupEnvironment(
? fs.readFileSync(globalClaudeMdPath, 'utf-8').trim()
: undefined;
const sessionClaudeMd = [
ownerCommonPlatformPrompt,
claudePlatformPrompt,
ownerCommonPairedRoomPrompt,
claudePairedRoomPrompt,
globalClaudeMemory,
options?.memoryBriefing,

View File

@@ -222,8 +222,8 @@ describe('runAgentForGroup room memory', () => {
expect(agentRunner.runAgentProcess).toHaveBeenCalledWith(
group,
expect.objectContaining({
prompt: expect.stringContaining(
'If you have no user-visible content to send for this turn, output exactly this JSON and nothing else: {"ejclaw":{"visibility":"silent"}}',
prompt: expect.stringMatching(
/If you have no user-visible content to send for this turn, output exactly this JSON and nothing else: \{"ejclaw":\{"visibility":"silent"\}\}[\s\S]*If you have already emitted any visible progress, status update, or partial answer earlier in this turn, do not end with the JSON object\. Finish with a short visible final conclusion for the user instead\./,
),
}),
expect.any(Function),
@@ -253,8 +253,8 @@ describe('runAgentForGroup room memory', () => {
expect(agentRunner.runAgentProcess).toHaveBeenCalledWith(
group,
expect.objectContaining({
prompt: expect.stringContaining(
'If you are only agreeing, mirroring, or restating without adding a concrete correction, risk, missing prerequisite, test gap, or code change, output only the JSON object.',
prompt: expect.stringMatching(
/If you have already emitted any visible progress, status update, or partial answer earlier in this turn, do not end with the JSON object\. Finish with a short visible final conclusion for the user instead\.[\s\S]*If you have not already emitted any visible progress, status update, or partial answer in this turn and you are only agreeing, mirroring, or restating without adding a concrete correction, risk, missing prerequisite, test gap, or code change, output only the JSON object\./,
),
}),
expect.any(Function),

View File

@@ -92,5 +92,16 @@ describe('buildStructuredOutputPrompt', () => {
expect(buildStructuredOutputPrompt('hello')).toContain(
'If you have no user-visible content to send for this turn, output exactly this JSON and nothing else: {"ejclaw":{"visibility":"silent"}}',
);
expect(buildStructuredOutputPrompt('hello')).toContain(
'If you have already emitted any visible progress, status update, or partial answer earlier in this turn, do not end with the JSON object. Finish with a short visible final conclusion for the user instead.',
);
});
it('tightens the reviewer silent rule when reviewer mode is enabled', () => {
expect(
buildStructuredOutputPrompt('hello', { reviewerMode: true }),
).toContain(
'If you have not already emitted any visible progress, status update, or partial answer in this turn and you are only agreeing, mirroring, or restating without adding a concrete correction, risk, missing prerequisite, test gap, or code change, output only the JSON object.',
);
});
});

View File

@@ -92,11 +92,12 @@ export function buildStructuredOutputPrompt(
`If you have no user-visible content to send for this turn, output exactly this JSON and nothing else: ${STRUCTURED_SILENT_OUTPUT_ENVELOPE}`,
'Do not wrap the JSON in backticks or code fences.',
'Do not combine the JSON with any other text.',
'If you have already emitted any visible progress, status update, or partial answer earlier in this turn, do not end with the JSON object. Finish with a short visible final conclusion for the user instead.',
];
if (options?.reviewerMode) {
lines.push(
'If you are only agreeing, mirroring, or restating without adding a concrete correction, risk, missing prerequisite, test gap, or code change, output only the JSON object.',
'If you have not already emitted any visible progress, status update, or partial answer in this turn and you are only agreeing, mirroring, or restating without adding a concrete correction, risk, missing prerequisite, test gap, or code change, output only the JSON object.',
);
}