feat(builtin-skills): add team-mode skill implementation with tests

This commit is contained in:
YeonGyu-Kim
2026-04-28 10:47:11 +09:00
parent d3c020c4f9
commit c9fdb04240
2 changed files with 242 additions and 0 deletions
@@ -0,0 +1,98 @@
import { describe, expect, test } from "bun:test"
import { createBuiltinSkills } from "../skills"
import { teamModeSkill } from "./team-mode"
describe("teamModeSkill gating", () => {
test("team-mode hidden when disabled", () => {
// given
const options = {
teamModeEnabled: false,
disabledSkills: new Set<string>(),
}
// when
const skills = createBuiltinSkills(options)
// then
expect(skills.some((skill) => skill.name === "team-mode")).toBe(false)
})
test("team-mode visible when enabled", () => {
// given
const options = {
teamModeEnabled: true,
disabledSkills: new Set<string>(),
}
// when
const skills = createBuiltinSkills(options)
// then
const skill = skills.find((candidateSkill) => candidateSkill.name === "team-mode")
expect(skill).toBeDefined()
expect(skill?.name).toBe("team-mode")
expect(skill?.description).toBe(teamModeSkill.description)
})
test("team-mode skill has no mcpConfig", () => {
// given
// when
const skill = teamModeSkill
// then
expect(skill.mcpConfig).toBeUndefined()
})
test("team-mode skill body keeps required keywords", () => {
// given
const body = teamModeSkill.template
// when
const keywords = [
"TeamSpec",
"member",
"category",
"subagent_type",
"sisyphus",
"atlas",
"hephaestus",
"oracle",
"eligible",
]
// then
for (const keyword of keywords) {
expect(body).toContain(keyword)
}
})
test("team-mode skill separates lead-only and member-safe tools", () => {
// given
const body = teamModeSkill.template
// when
const leadOnlyTools = ["team_create", "team_delete", "team_shutdown_request"]
const universalTools = [
"team_send_message",
"team_task_create",
"team_task_list",
"team_task_update",
"team_task_get",
"team_status",
]
// then
expect(body).toContain("## Lead-only tools")
expect(body).toContain("## Universal team-run tools")
expect(body).toContain("## Global query tool")
for (const toolName of leadOnlyTools) {
expect(body).toContain(toolName)
}
for (const toolName of universalTools) {
expect(body).toContain(toolName)
}
expect(body).not.toContain("team_shutdown_request - ask the lead to wind down")
})
})
@@ -0,0 +1,144 @@
import type { BuiltinSkill } from "../types"
export const teamModeSkill: BuiltinSkill = {
name: "team-mode",
description:
"Team orchestration — create and manage parallel agent teams (OFF by default; enable via team_mode.enabled in config). Loading this skill provides usage documentation; the team_* tools are registered globally when team_mode.enabled=true and access-gated by team role.",
template: `# Team Mode
Team mode gives Claude Code Agent Teams parity. It is off by default. Enable it only when you want parallel multi-agent coordination, where each team member is an opencode child session.
## When to use
- Split a large job across several agents.
- Keep a lead agent focused while member agents work in parallel.
- Use worktree mode for isolated code changes, or tmux visualization when you want live session layout.
## Declare a team
Create a team at \`~/.omo/teams/{name}/config.json\`.
You can also pass the same object directly to \`team_create({ inline_spec: ... })\`.
This TeamSpec uses a lead plus members list. Every canonical member has a \`kind\` discriminator.
Example:
\`\`\`json
{
"name": "release-squad",
"lead": {
"kind": "subagent_type",
"subagent_type": "sisyphus"
},
"members": [
{
"kind": "category",
"category": "quick",
"prompt": "review small changes and report risks"
},
{
"kind": "subagent_type",
"subagent_type": "atlas"
}
]
}
\`\`\`
Inline shorthand is accepted for category members. If \`kind\` is omitted, \`category\` implies \`kind: "category"\`. If a member uses natural planning fields like \`role\`, \`description\`, \`capabilities\`, or an unknown \`kind\`, it becomes a category worker using the current config's first enabled category. If \`kind\` is an unknown string such as a category name, that string is used as the category. \`systemPrompt\` is accepted as a \`prompt\` alias, and \`loadSkills\` is ignored because team members receive their behavior through \`prompt\`.
Example:
\`\`\`json
{
"name": "project-analysis-team",
"members": [
{
"name": "structure-analyst",
"category": "quick",
"systemPrompt": "Analyze directory layouts, module boundaries, and architectural organization."
},
{
"name": "quality-analyst",
"category": "quick",
"systemPrompt": "Analyze tests, CI/CD, build scripts, conventions, and anti-patterns."
},
{
"name": "Agent 3: Quality/Process Analyst",
"role": "Quality/Process Analyst",
"capabilities": ["tests", "builds", "CI/CD"]
}
]
}
\`\`\`
## Member schema
Use \`kind: "category"\` when you want a category-backed worker. It must include both \`category\` and \`prompt\`. D-40: category members always route through \`sisyphus-junior\`.
Use \`kind: "subagent_type"\` only for eligible agents.
### Eligible subagent types
- \`sisyphus\`
- \`atlas\`
- \`sisyphus-junior\`
- \`hephaestus\`
### Hard rejects
Do not use \`oracle\`, \`prometheus\`, or other non-eligible agents here. For those, use \`delegate-task\` instead.
## Lifecycle
1. Lead creates the team with \`team_create({ teamName: "existing-team" })\` or \`team_create({ inline_spec: { name: "team-name", members: [...] } })\`. Never call \`team_create\` with empty arguments.
2. Lead assigns work with \`team_send_message\` or \`team_task_create\`.
3. Members report progress with \`team_send_message\` plus \`team_task_update\`.
4. Lead and members track progress with \`team_task_list\`, \`team_task_get\`, and \`team_status\`.
5. Lead requests shutdown with \`team_shutdown_request\` when the team is ready to wind down.
6. The targeted member or the lead handles \`team_approve_shutdown\` or \`team_reject_shutdown\`.
7. Lead removes the team with \`team_delete\`.
## Lead-only tools
- \`team_create\` - create a team from a declaration.
- \`team_delete\` - remove a team.
- \`team_shutdown_request\` - start the shutdown flow.
## Lead or target-member shutdown tools
- \`team_approve_shutdown\` - approve shutdown for the targeted member.
- \`team_reject_shutdown\` - reject shutdown for the targeted member.
## Universal team-run tools
- \`team_send_message\` - send a direct message; broadcast is still lead-only.
- \`team_task_create\` - create a task for a member.
- \`team_task_list\` - list team tasks.
- \`team_task_update\` - update task state.
- \`team_task_get\` - inspect one task.
- \`team_status\` - show live team status.
## Global query tool
- \`team_list\` - list known teams.
## Bounds
- Max 8 members.
- Max 4 parallel workers.
- Max 32KB per message.
- Max 256KB unread inbox.
## Failure modes
- Broadcast is lead-only.
- No nested teams.
- No peer sync wait; work moves asynchronously.
## Notes
Team mode is a docs-only skill. The team_* tools are registered globally when \`team_mode.enabled=true\`.
Use \`~/.omo/teams/{name}/config.json\` plus worktree or tmux visibility to understand how the team is laid out.
`,
}