fix(ci): stabilize opencode delivery checks

This commit is contained in:
777genius 2026-05-05 01:16:55 +03:00
parent c8edfc6026
commit 7ab57bc8be
6 changed files with 46 additions and 35 deletions

View file

@ -14,7 +14,7 @@ export function registerMessageTools(server: Pick<FastMCP, 'addTool'>) {
server.addTool({
name: 'message_send',
description:
'Send a visible team/user message into team inbox. OpenCode teammates should use this for normal replies to the human user, lead, or same-team teammates. from is required and must be your configured teammate name; user is reserved for app-owned writes. When replying to an app-delivered OpenCode runtime message, include source="runtime_delivery" and relayOfMessageId with the inbound app messageId. Do not invent placeholder task refs. If the message is not about a real board task, omit # task labels; never use #00000000.',
'Send a visible team/user message into team inbox. OpenCode teammates should use this for normal replies to the human user, lead, or same-team teammates. from is required and must be your configured teammate name; user is reserved for app-owned writes. When replying to an app-delivered OpenCode runtime message, include source="runtime_delivery" and relayOfMessageId with the inbound app messageId. After a successful app-delivered runtime reply, stop and do not send the same answer again. Do not invent placeholder task refs. If the message is not about a real board task, omit # task labels; never use #00000000.',
parameters: z.object({
...toolContextSchema,
to: z.string().min(1),
@ -58,21 +58,26 @@ export function registerMessageTools(server: Pick<FastMCP, 'addTool'>) {
taskRefs,
}) => {
assertConfiguredTeam(teamName, claudeDir);
return await Promise.resolve(
jsonTextContent(
getController(teamName, claudeDir).messages.sendMessage({
to,
text,
...(from ? { from } : {}),
...(summary ? { summary } : {}),
...(source ? { source } : {}),
...(relayOfMessageId ? { relayOfMessageId } : {}),
...(leadSessionId ? { leadSessionId } : {}),
...(attachments?.length ? { attachments } : {}),
...(taskRefs?.length ? { taskRefs } : {}),
})
)
);
const result = getController(teamName, claudeDir).messages.sendMessage({
to,
text,
...(from ? { from } : {}),
...(summary ? { summary } : {}),
...(source ? { source } : {}),
...(relayOfMessageId ? { relayOfMessageId } : {}),
...(leadSessionId ? { leadSessionId } : {}),
...(attachments?.length ? { attachments } : {}),
...(taskRefs?.length ? { taskRefs } : {}),
});
const protocolInstruction =
source === 'runtime_delivery' || relayOfMessageId
? 'Delivered as an app-delivered runtime reply. Stop this turn now; do not call message_send again for the same inbound message.'
: 'Delivered. If this answered one app/user instruction, do not call message_send again for the same answer.';
const payload =
result && typeof result === 'object' && !Array.isArray(result)
? { ...(result as Record<string, unknown>), protocolInstruction }
: { result, protocolInstruction };
return await Promise.resolve(jsonTextContent(payload));
},
});
}

View file

@ -1427,6 +1427,7 @@ describe('agent-teams-mcp tools', () => {
);
expect(sent.deliveredToInbox).toBe(true);
expect(sent.protocolInstruction).toContain('do not call message_send again');
const inboxPath = path.join(claudeDir, 'teams', teamName, 'inboxes', 'alice.json');
const rows = JSON.parse(fs.readFileSync(inboxPath, 'utf8'));
expect(rows[0].source).toBe('system_notification');

View file

@ -143,7 +143,7 @@ function extractOpenCodeMemberSessionRecordedAt(
diagnostics: readonly string[] | undefined
): string[] {
return (diagnostics ?? []).flatMap((diagnostic) => {
const match = diagnostic.match(OPENCODE_MEMBER_SESSION_RECORDED_AT_PATTERN);
const match = OPENCODE_MEMBER_SESSION_RECORDED_AT_PATTERN.exec(diagnostic);
return match?.[1] ? [match[1]] : [];
});
}

View file

@ -2260,7 +2260,7 @@ function extractOpenCodeMemberSessionRecordedAt(
diagnostics: readonly string[] | undefined
): string[] {
return (diagnostics ?? []).flatMap((diagnostic) => {
const match = diagnostic.match(OPENCODE_MEMBER_SESSION_RECORDED_AT_PATTERN);
const match = OPENCODE_MEMBER_SESSION_RECORDED_AT_PATTERN.exec(diagnostic);
return match?.[1] ? [match[1]] : [];
});
}

View file

@ -85,7 +85,7 @@ const REQUIRED_READY_CHECKPOINTS = new Set([
const GENERIC_OPEN_CODE_MEMBER_FAILURE_REASON = 'OpenCode bridge reported member launch failure';
const SECRET_FLAG_PATTERN =
/(--(?:api-key|token|password|secret|authorization|auth-token)(?:=|\s+))("[^"]*"|'[^']*'|\S+)/gi;
const BEARER_TOKEN_PATTERN = /\bBearer\s+[A-Za-z0-9._~+/=-]+/gi;
const BEARER_TOKEN_PATTERN = /\bBearer\s+\S+/gi;
const SECRET_KEY_PATTERN = /\bsk-[A-Za-z0-9_-]{16,}\b/g;
export class OpenCodeTeamRuntimeAdapter implements TeamLaunchRuntimeAdapter {
@ -797,6 +797,7 @@ function buildOpenCodeRuntimeMessageText(input: OpenCodeTeamRuntimeMessageInput)
input.messageId
? `Include relayOfMessageId="${input.messageId}" in that message_send call.`
: null,
'After the message_send tool call succeeds, stop immediately. Do not send follow-up confirmations or repeat the same answer.',
'Do not call runtime_bootstrap_checkin or member_briefing just to answer this delivered app message.',
'Do not answer only with plain assistant text when agent-teams_message_send is available.',
'Do not use SendMessage or runtime_deliver_message for ordinary visible replies.',

View file

@ -4620,47 +4620,51 @@ describe('TeamDataService', () => {
});
it('keeps member branch enrichment on by default for full UI team data snapshots', async () => {
const rootRepoPath = path.normalize('/repo');
const aliceRepoPath = path.normalize('/repo-alice');
const getBranchSpy = vi
.spyOn(gitIdentityResolver, 'getBranch')
.mockImplementation(async (cwd) => (cwd === '/repo-alice' ? 'feature/alice' : 'main'));
.mockImplementation(async (cwd) => (cwd === aliceRepoPath ? 'feature/alice' : 'main'));
const harness = createGetTeamDataHarness({
config: buildDefaultTeamConfig({
projectPath: '/repo',
projectPath: rootRepoPath,
members: [
{ name: 'team-lead', role: 'Lead', cwd: '/repo' },
{ name: 'alice', role: 'Developer', cwd: '/repo-alice' },
{ name: 'team-lead', role: 'Lead', cwd: rootRepoPath },
{ name: 'alice', role: 'Developer', cwd: aliceRepoPath },
],
}),
resolveMembers: () => [
{ ...buildResolvedMember('team-lead'), cwd: '/repo' },
{ ...buildResolvedMember('alice'), cwd: '/repo-alice' },
{ ...buildResolvedMember('team-lead'), cwd: rootRepoPath },
{ ...buildResolvedMember('alice'), cwd: aliceRepoPath },
],
});
const data = await harness.service.getTeamData('my-team');
expect(getBranchSpy).toHaveBeenCalledWith('/repo');
expect(getBranchSpy).toHaveBeenCalledWith('/repo-alice');
expect(getBranchSpy).toHaveBeenCalledWith(rootRepoPath);
expect(getBranchSpy).toHaveBeenCalledWith(aliceRepoPath);
expect(data.members.find((member) => member.name === 'alice')?.gitBranch).toBe(
'feature/alice'
);
});
it('keeps member branch enrichment on for explicit full UI team data snapshots', async () => {
const rootRepoPath = path.normalize('/repo');
const aliceRepoPath = path.normalize('/repo-alice');
const getBranchSpy = vi
.spyOn(gitIdentityResolver, 'getBranch')
.mockImplementation(async (cwd) => (cwd === '/repo-alice' ? 'feature/alice' : 'main'));
.mockImplementation(async (cwd) => (cwd === aliceRepoPath ? 'feature/alice' : 'main'));
const harness = createGetTeamDataHarness({
config: buildDefaultTeamConfig({
projectPath: '/repo',
projectPath: rootRepoPath,
members: [
{ name: 'team-lead', role: 'Lead', cwd: '/repo' },
{ name: 'alice', role: 'Developer', cwd: '/repo-alice' },
{ name: 'team-lead', role: 'Lead', cwd: rootRepoPath },
{ name: 'alice', role: 'Developer', cwd: aliceRepoPath },
],
}),
resolveMembers: () => [
{ ...buildResolvedMember('team-lead'), cwd: '/repo' },
{ ...buildResolvedMember('alice'), cwd: '/repo-alice' },
{ ...buildResolvedMember('team-lead'), cwd: rootRepoPath },
{ ...buildResolvedMember('alice'), cwd: aliceRepoPath },
],
});
@ -4668,8 +4672,8 @@ describe('TeamDataService', () => {
includeMemberBranches: true,
});
expect(getBranchSpy).toHaveBeenCalledWith('/repo');
expect(getBranchSpy).toHaveBeenCalledWith('/repo-alice');
expect(getBranchSpy).toHaveBeenCalledWith(rootRepoPath);
expect(getBranchSpy).toHaveBeenCalledWith(aliceRepoPath);
expect(data.members.find((member) => member.name === 'alice')?.gitBranch).toBe(
'feature/alice'
);