feat(team): harden messaging tools and runtime bootstrap

This commit is contained in:
777genius 2026-04-24 23:44:44 +03:00
parent e6e3ae9f54
commit dec0eaba18
8 changed files with 54 additions and 8 deletions

View file

@ -1,6 +1,27 @@
const messageStore = require('./messageStore.js'); const messageStore = require('./messageStore.js');
const runtimeHelpers = require('./runtimeHelpers.js'); const runtimeHelpers = require('./runtimeHelpers.js');
const PLACEHOLDER_TASK_REF_PREFIX = /^\s*#0{8}\b\s*(?:[:.-]\s*)?/i;
function stripPlaceholderTaskRefPrefix(value) {
if (typeof value !== 'string' || !PLACEHOLDER_TASK_REF_PREFIX.test(value)) {
return value;
}
return value.replace(PLACEHOLDER_TASK_REF_PREFIX, '').trimStart();
}
function normalizePlaceholderTaskRefPrefixes(flags) {
const next = { ...(flags || {}) };
if (typeof next.text === 'string') {
const strippedText = stripPlaceholderTaskRefPrefix(next.text);
next.text = strippedText.trim() ? strippedText : next.text;
}
if (typeof next.summary === 'string') {
next.summary = stripPlaceholderTaskRefPrefix(next.summary);
}
return next;
}
function normalizeMessageSendFlags(context, flags) { function normalizeMessageSendFlags(context, flags) {
const next = { ...(flags || {}) }; const next = { ...(flags || {}) };
const rawTo = const rawTo =
@ -61,7 +82,7 @@ function assertUserDirectedMessageHasSender(context, flags) {
} }
function sendMessage(context, flags) { function sendMessage(context, flags) {
const normalized = normalizeMessageSendFlags(context, flags); const normalized = normalizeMessageSendFlags(context, normalizePlaceholderTaskRefPrefixes(flags));
assertUserDirectedMessageHasSender(context, normalized); assertUserDirectedMessageHasSender(context, normalized);
return messageStore.sendInboxMessage(context.paths, normalized); return messageStore.sendInboxMessage(context.paths, normalized);
} }

View file

@ -684,6 +684,8 @@ function buildMemberTaskProtocol(teamName, messagingProtocol = createMemberMessa
{ teamName: "${teamName}", taskId: "<taskId>", text: "<summary of your finding or decision>", from: "<your-name>" } { teamName: "${teamName}", taskId: "<taskId>", text: "<summary of your finding or decision>", from: "<your-name>" }
Do NOT comment on trivial coordination messages. Only comment when the information is valuable context for the task. Do NOT comment on trivial coordination messages. Only comment when the information is valuable context for the task.
9. When sending a message about a specific task, include its short display label like #<displayId> in your ${messagingProtocol.sendToolName} summary field for traceability. 9. When sending a message about a specific task, include its short display label like #<displayId> in your ${messagingProtocol.sendToolName} summary field for traceability.
- If the message is NOT about a real board task, do NOT include any # task label.
- Never invent placeholder task refs such as #00000000 or #<displayId>.
10. In ALL human-facing or teammate-facing message text, when you mention a task reference, ALWAYS write it with a leading # (for example: #abcd1234, not abcd1234 or "task abcd1234"). 10. In ALL human-facing or teammate-facing message text, when you mention a task reference, ALWAYS write it with a leading # (for example: #abcd1234, not abcd1234 or "task abcd1234").
11. Review workflow clarity (IMPORTANT): 11. Review workflow clarity (IMPORTANT):
- The work task (e.g. #1) is the thing that must end up APPROVED after review. - The work task (e.g. #1) is the thing that must end up APPROVED after review.

View file

@ -177,10 +177,28 @@ describe('agent-teams-controller API', () => {
'agent-teams_message_send { teamName: "my-team", to: "alice", from: "bob"' 'agent-teams_message_send { teamName: "my-team", to: "alice", from: "bob"'
); );
expect(briefing).toContain('Full details in task comment e5f6a7b8'); expect(briefing).toContain('Full details in task comment e5f6a7b8');
expect(briefing).toContain('Never invent placeholder task refs such as #00000000');
expect(briefing).not.toContain('task_get_comment {'); expect(briefing).not.toContain('task_get_comment {');
expect(briefing).not.toContain('notify your team lead via SendMessage'); expect(briefing).not.toContain('notify your team lead via SendMessage');
}); });
it('strips hallucinated zero task placeholder prefixes from visible messages', () => {
const claudeDir = makeClaudeDir();
const controller = createController({ teamName: 'my-team', claudeDir });
controller.messages.sendMessage({
to: 'user',
from: 'bob',
text: '#00000000 bootstrap check-in and briefing retrieved. No actionable tasks.',
summary: '#00000000 ready',
});
const userInboxPath = path.join(claudeDir, 'teams', 'my-team', 'inboxes', 'user.json');
const rows = JSON.parse(fs.readFileSync(userInboxPath, 'utf8'));
expect(rows[0].text).toBe('bootstrap check-in and briefing retrieved. No actionable tasks.');
expect(rows[0].summary).toBe('ready');
});
it('does not infer OpenCode briefing from a generic provider-scoped model alone', async () => { it('does not infer OpenCode briefing from a generic provider-scoped model alone', async () => {
const claudeDir = makeClaudeDir(); const claudeDir = makeClaudeDir();
const configPath = path.join(claudeDir, 'teams', 'my-team', 'config.json'); const configPath = path.join(claudeDir, 'teams', 'my-team', 'config.json');

View file

@ -14,7 +14,7 @@ export function registerMessageTools(server: Pick<FastMCP, 'addTool'>) {
server.addTool({ server.addTool({
name: 'message_send', name: 'message_send',
description: 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. When to is "user", from is required and must be your configured teammate name.', '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. When to is "user", from is required and must be your configured teammate name. 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({ parameters: z.object({
...toolContextSchema, ...toolContextSchema,
to: z.string().min(1), to: z.string().min(1),

View file

@ -592,6 +592,9 @@ describe('agent-teams-mcp tools', () => {
).content[0]?.text; ).content[0]?.text;
expect(openCodeMemberBriefingText).toContain('agent-teams_message_send'); expect(openCodeMemberBriefingText).toContain('agent-teams_message_send');
expect(openCodeMemberBriefingText).toContain('Full details in task comment e5f6a7b8'); expect(openCodeMemberBriefingText).toContain('Full details in task comment e5f6a7b8');
expect(openCodeMemberBriefingText).toContain(
'Never invent placeholder task refs such as #00000000'
);
expect(openCodeMemberBriefingText).not.toContain('task_get_comment {'); expect(openCodeMemberBriefingText).not.toContain('task_get_comment {');
expect(openCodeMemberBriefingText).not.toContain('notify your team lead via SendMessage'); expect(openCodeMemberBriefingText).not.toContain('notify your team lead via SendMessage');
}); });

View file

@ -1,27 +1,27 @@
{ {
"version": "0.0.7", "version": "0.0.8",
"sourceRef": "v0.0.7", "sourceRef": "v0.0.8",
"sourceRepository": "777genius/agent_teams_orchestrator", "sourceRepository": "777genius/agent_teams_orchestrator",
"releaseRepository": "777genius/claude_agent_teams_ui", "releaseRepository": "777genius/claude_agent_teams_ui",
"releaseTag": "v1.2.0", "releaseTag": "v1.2.0",
"assets": { "assets": {
"darwin-arm64": { "darwin-arm64": {
"file": "agent-teams-runtime-darwin-arm64-v0.0.7.tar.gz", "file": "agent-teams-runtime-darwin-arm64-v0.0.8.tar.gz",
"archiveKind": "tar.gz", "archiveKind": "tar.gz",
"binaryName": "claude-multimodel" "binaryName": "claude-multimodel"
}, },
"darwin-x64": { "darwin-x64": {
"file": "agent-teams-runtime-darwin-x64-v0.0.7.tar.gz", "file": "agent-teams-runtime-darwin-x64-v0.0.8.tar.gz",
"archiveKind": "tar.gz", "archiveKind": "tar.gz",
"binaryName": "claude-multimodel" "binaryName": "claude-multimodel"
}, },
"linux-x64": { "linux-x64": {
"file": "agent-teams-runtime-linux-x64-v0.0.7.tar.gz", "file": "agent-teams-runtime-linux-x64-v0.0.8.tar.gz",
"archiveKind": "tar.gz", "archiveKind": "tar.gz",
"binaryName": "claude-multimodel" "binaryName": "claude-multimodel"
}, },
"win32-x64": { "win32-x64": {
"file": "agent-teams-runtime-win32-x64-v0.0.7.zip", "file": "agent-teams-runtime-win32-x64-v0.0.8.zip",
"archiveKind": "zip", "archiveKind": "zip",
"binaryName": "claude-multimodel.exe" "binaryName": "claude-multimodel.exe"
} }

View file

@ -585,6 +585,7 @@ function buildOpenCodeRuntimeMessageText(input: OpenCodeTeamRuntimeMessageInput)
`Use teamName="${input.teamName}", to="${replyRecipient}", from="${input.memberName}", text, and summary.`, `Use teamName="${input.teamName}", to="${replyRecipient}", from="${input.memberName}", text, and summary.`,
'Do not answer only with plain assistant text when agent-teams_message_send is available.', '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.', 'Do not use SendMessage or runtime_deliver_message for ordinary visible replies.',
'Do not invent placeholder task labels. If no explicit taskRefs are provided and the reply is not about a real board task, do not prefix text or summary with a # task label; never use #00000000.',
input.actionMode ? `Action mode for this message: ${input.actionMode}.` : null, input.actionMode ? `Action mode for this message: ${input.actionMode}.` : null,
taskRefs ? `If your reply is about these tasks, include taskRefs exactly: ${taskRefs}` : null, taskRefs ? `If your reply is about these tasks, include taskRefs exactly: ${taskRefs}` : null,
input.messageId input.messageId

View file

@ -359,6 +359,7 @@ describe('OpenCodeTeamRuntimeAdapter', () => {
'If your reply is about these tasks, include taskRefs exactly: [{"taskId":"task-1","displayId":"abcd1234","teamName":"team-a"}]' 'If your reply is about these tasks, include taskRefs exactly: [{"taskId":"task-1","displayId":"abcd1234","teamName":"team-a"}]'
); );
expect(sentText).toContain('Do not use SendMessage or runtime_deliver_message'); expect(sentText).toContain('Do not use SendMessage or runtime_deliver_message');
expect(sentText).toContain('never use #00000000');
}); });
it('does not parse legacy native SendMessage wording to infer OpenCode reply recipient', async () => { it('does not parse legacy native SendMessage wording to infer OpenCode reply recipient', async () => {