fix(ci): stabilize opencode delivery checks
This commit is contained in:
parent
c8edfc6026
commit
7ab57bc8be
6 changed files with 46 additions and 35 deletions
|
|
@ -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));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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]] : [];
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]] : [];
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue