feat: unify 3 bot services into single process

- Add UNIFIED_MODE flag (default on, disable with UNIFIED_MODE=0)
- Register all 3 Discord bots in one process (claude, codex, review)
- Load all registered groups regardless of agent_type (codex/owner priority)
- Start credential proxy and container cleanup at unified startup
- shouldServiceProcessChat returns true in unified mode
- Add findChannelByName for role-based response routing
- Backward compatible: UNIFIED_MODE=0 restores per-service behavior
This commit is contained in:
Eyejoker
2026-03-29 18:22:57 +09:00
parent fc9f2867b9
commit d01f98bd61
20 changed files with 118 additions and 759 deletions

View File

@@ -304,7 +304,7 @@ describe('runAgentForGroup room memory', () => {
);
});
it('requires a visible structured verdict in reviewer gate turns', async () => {
it('allows silent reviewer outputs', async () => {
const group = { ...makeGroup(), folder: 'test-group', workDir: '/repo' };
vi.mocked(serviceRouting.getEffectiveChannelLease).mockReturnValue({
chat_jid: 'group@test',
@@ -325,112 +325,21 @@ describe('runAgentForGroup room memory', () => {
reviewer_service_id: 'claude',
title: null,
source_ref: 'HEAD',
task_policy: 'autonomous',
risk_level: 'low',
plan_status: 'not_requested',
review_requested_at: null,
gate_turn_kind: 'implementation_start',
reviewer_verdict: null,
reviewer_verdict_at: null,
reviewer_verdict_note: null,
plan_notes: null,
status: 'active',
created_at: '2026-03-29T00:00:00.000Z',
updated_at: '2026-03-29T00:00:00.000Z',
},
execution: {
id: 'run-gate:claude',
task_id: 'paired-task-gate',
service_id: 'claude',
role: 'reviewer',
workspace_id: null,
status: 'running',
summary: null,
created_at: '2026-03-29T00:00:00.000Z',
started_at: '2026-03-29T00:00:00.000Z',
completed_at: null,
},
workspace: null,
envOverrides: {},
gateTurnKind: 'implementation_start',
requiresVisibleVerdict: true,
});
await runAgentForGroup(makeDeps(), {
group,
prompt: 'hello',
chatJid: 'group@test',
runId: 'run-review-gate-prompt',
});
expect(agentRunner.runAgentProcess).toHaveBeenCalledWith(
group,
expect.objectContaining({
prompt: expect.stringContaining(
'This turn is a paired-room gate turn for implementation_start. Silent output is forbidden.',
),
}),
expect.any(Function),
undefined,
{},
);
});
it('blocks silent reviewer output on gate turns and records a silent verdict', async () => {
const group = { ...makeGroup(), folder: 'test-group', workDir: '/repo' };
vi.mocked(serviceRouting.getEffectiveChannelLease).mockReturnValue({
chat_jid: 'group@test',
owner_service_id: 'codex-main',
reviewer_service_id: 'claude',
activated_at: null,
reason: null,
explicit: false,
});
vi.mocked(
pairedExecutionContext.preparePairedExecutionContext,
).mockReturnValue({
task: {
id: 'paired-task-gate',
chat_jid: 'group@test',
group_folder: 'test-group',
owner_service_id: 'codex-main',
reviewer_service_id: 'claude',
title: null,
source_ref: 'HEAD',
task_policy: 'autonomous',
risk_level: 'low',
plan_status: 'not_requested',
review_requested_at: null,
gate_turn_kind: 'implementation_start',
reviewer_verdict: null,
reviewer_verdict_at: null,
reviewer_verdict_note: null,
status: 'active',
created_at: '2026-03-29T00:00:00.000Z',
updated_at: '2026-03-29T00:00:00.000Z',
},
execution: {
id: 'run-gate:claude',
task_id: 'paired-task-gate',
service_id: 'claude',
role: 'reviewer',
workspace_id: null,
status: 'running',
summary: null,
created_at: '2026-03-29T00:00:00.000Z',
started_at: '2026-03-29T00:00:00.000Z',
completed_at: null,
},
workspace: null,
envOverrides: {},
gateTurnKind: 'implementation_start',
requiresVisibleVerdict: true,
});
vi.mocked(agentRunner.runAgentProcess).mockImplementation(
async (_group, _input, _onProcess, onOutput) => {
await onOutput?.({
status: 'success',
result: null,
output: { visibility: 'silent', verdict: 'silent' },
output: { visibility: 'silent' },
phase: 'final',
});
return {
@@ -445,190 +354,7 @@ describe('runAgentForGroup room memory', () => {
group,
prompt: 'hello',
chatJid: 'group@test',
runId: 'run-review-gate-silent',
onOutput: async (output) => {
outputs.push(output);
},
});
expect(result).toBe('error');
expect(outputs.at(-1)?.output).toEqual({
visibility: 'public',
text: expect.stringContaining(
'Reviewer gate turn requires a visible structured verdict.',
),
});
expect(
pairedExecutionContext.completePairedExecutionContext,
).toHaveBeenCalledWith(
expect.objectContaining({
executionId: 'run-gate:claude',
status: 'failed',
reviewerVerdict: 'silent',
}),
);
});
it('allows structured reviewer verdicts on gate turns', async () => {
const group = { ...makeGroup(), folder: 'test-group', workDir: '/repo' };
vi.mocked(serviceRouting.getEffectiveChannelLease).mockReturnValue({
chat_jid: 'group@test',
owner_service_id: 'codex-main',
reviewer_service_id: 'claude',
activated_at: null,
reason: null,
explicit: false,
});
vi.mocked(
pairedExecutionContext.preparePairedExecutionContext,
).mockReturnValue({
task: {
id: 'paired-task-gate',
chat_jid: 'group@test',
group_folder: 'test-group',
owner_service_id: 'codex-main',
reviewer_service_id: 'claude',
title: null,
source_ref: 'HEAD',
task_policy: 'autonomous',
risk_level: 'low',
plan_status: 'not_requested',
review_requested_at: null,
gate_turn_kind: 'implementation_start',
reviewer_verdict: null,
reviewer_verdict_at: null,
reviewer_verdict_note: null,
status: 'active',
created_at: '2026-03-29T00:00:00.000Z',
updated_at: '2026-03-29T00:00:00.000Z',
},
execution: {
id: 'run-gate:claude',
task_id: 'paired-task-gate',
service_id: 'claude',
role: 'reviewer',
workspace_id: null,
status: 'running',
summary: null,
created_at: '2026-03-29T00:00:00.000Z',
started_at: '2026-03-29T00:00:00.000Z',
completed_at: null,
},
workspace: null,
envOverrides: {},
gateTurnKind: 'implementation_start',
requiresVisibleVerdict: true,
});
vi.mocked(agentRunner.runAgentProcess).mockImplementation(
async (_group, _input, _onProcess, onOutput) => {
await onOutput?.({
status: 'success',
result: null,
output: {
visibility: 'public',
verdict: 'done_with_concerns',
text: '**DONE_WITH_CONCERNS**\n문제는 없지만 확인 포인트는 있습니다.',
},
phase: 'final',
});
return {
status: 'success',
result: null,
};
},
);
const result = await runAgentForGroup(makeDeps(), {
group,
prompt: 'hello',
chatJid: 'group@test',
runId: 'run-review-gate-done',
onOutput: async () => {},
});
expect(result).toBe('success');
expect(
pairedExecutionContext.completePairedExecutionContext,
).toHaveBeenCalledWith(
expect.objectContaining({
executionId: 'run-gate:claude',
status: 'succeeded',
reviewerVerdict: 'done_with_concerns',
}),
);
});
it('still allows silent reviewer outputs on non-gate turns', async () => {
const group = { ...makeGroup(), folder: 'test-group', workDir: '/repo' };
vi.mocked(serviceRouting.getEffectiveChannelLease).mockReturnValue({
chat_jid: 'group@test',
owner_service_id: 'codex-main',
reviewer_service_id: 'claude',
activated_at: null,
reason: null,
explicit: false,
});
vi.mocked(
pairedExecutionContext.preparePairedExecutionContext,
).mockReturnValue({
task: {
id: 'paired-task-gate',
chat_jid: 'group@test',
group_folder: 'test-group',
owner_service_id: 'codex-main',
reviewer_service_id: 'claude',
title: null,
source_ref: 'HEAD',
task_policy: 'autonomous',
risk_level: 'low',
plan_status: 'not_requested',
review_requested_at: null,
gate_turn_kind: null,
reviewer_verdict: null,
reviewer_verdict_at: null,
reviewer_verdict_note: null,
status: 'active',
created_at: '2026-03-29T00:00:00.000Z',
updated_at: '2026-03-29T00:00:00.000Z',
},
execution: {
id: 'run-gate:claude',
task_id: 'paired-task-gate',
service_id: 'claude',
role: 'reviewer',
workspace_id: null,
status: 'running',
summary: null,
created_at: '2026-03-29T00:00:00.000Z',
started_at: '2026-03-29T00:00:00.000Z',
completed_at: null,
},
workspace: null,
envOverrides: {},
gateTurnKind: null,
requiresVisibleVerdict: false,
});
vi.mocked(agentRunner.runAgentProcess).mockImplementation(
async (_group, _input, _onProcess, onOutput) => {
await onOutput?.({
status: 'success',
result: null,
output: { visibility: 'silent', verdict: 'silent' },
phase: 'final',
});
return {
status: 'success',
result: null,
};
},
);
const outputs: AgentOutput[] = [];
const result = await runAgentForGroup(makeDeps(), {
group,
prompt: 'hello',
chatJid: 'group@test',
runId: 'run-review-non-gate-silent',
runId: 'run-review-silent',
onOutput: async (output) => {
outputs.push(output);
},
@@ -637,7 +363,7 @@ describe('runAgentForGroup room memory', () => {
expect(result).toBe('success');
expect(outputs).toEqual([
expect.objectContaining({
output: { visibility: 'silent', verdict: 'silent' },
output: { visibility: 'silent' },
}),
]);
});