Merge pull request #4381 from code-yeongyu/fix/team-mode-closure-prompts

fix(team-mode): make lead close teams on its own initiative
This commit is contained in:
YeonGyu-Kim
2026-05-24 15:37:28 +09:00
committed by GitHub
7 changed files with 35 additions and 106 deletions
+1 -1
View File
@@ -13,7 +13,7 @@ Transform Tier hook on `messages.transform`. Scans first user message for mode k
| `ultrawork` / `ulw` | `/\b(ultrawork|ulw)\b/i` | Full orchestration mode — parallel agents, deep exploration, relentless execution |
| Search mode | `SEARCH_PATTERN` (from `search/`) | Web/doc search focus prompt injection |
| Analyze mode | `ANALYZE_PATTERN` (from `analyze/`) | Deep analysis mode prompt injection |
| Team mode | `TEAM_PATTERN` (from `team/`) | Forces orchestration via `team_*` tools when user invokes `team mode` / `팀 모드` / `팀으로`; instructs user to enable `team_mode.enabled` if tools are absent |
| Team mode | `TEAM_PATTERN` (from `team/`) | Forces orchestration via `team_*` tools when user invokes `team mode` / `team-mode` / `team_mode` / `teammode`; instructs user to enable `team_mode.enabled` if tools are absent and reminds lead to run the closure sequence once every task is terminal |
## STRUCTURE
-84
View File
@@ -1062,90 +1062,6 @@ describe("keyword-detector team mode", () => {
expect(textPart!.text).toContain("for this task")
})
test("should inject team-mode message when user types '팀 모드' (Korean with space)", async () => {
// given - main session typing Korean '팀 모드'
const collector = new ContextCollector()
const sessionID = "team-ko-spaced-session"
getMainSessionSpy = spyOn(sessionState, "getMainSessionID").mockReturnValue(sessionID)
const hook = createKeywordDetectorHook(createMockPluginInput(), collector)
const output = {
message: {} as Record<string, unknown>,
parts: [{ type: "text", text: "이거 팀 모드로 해줘" }],
}
// when - keyword detection runs
await hook["chat.message"]({ sessionID }, output)
// then - team-mode message should be prepended
const textPart = output.parts.find(p => p.type === "text")
expect(textPart).toBeDefined()
expect(textPart!.text).toContain("[team-mode]")
expect(textPart!.text).toContain("팀 모드로 해줘")
})
test("should inject team-mode message when user types '팀으로'", async () => {
// given - main session typing Korean '팀으로'
const collector = new ContextCollector()
const sessionID = "team-ko-eulo-session"
getMainSessionSpy = spyOn(sessionState, "getMainSessionID").mockReturnValue(sessionID)
const hook = createKeywordDetectorHook(createMockPluginInput(), collector)
const output = {
message: {} as Record<string, unknown>,
parts: [{ type: "text", text: "팀으로 일하자" }],
}
// when - keyword detection runs
await hook["chat.message"]({ sessionID }, output)
// then - team-mode message should be prepended
const textPart = output.parts.find(p => p.type === "text")
expect(textPart).toBeDefined()
expect(textPart!.text).toContain("[team-mode]")
expect(textPart!.text).toContain("팀으로 일하자")
})
test("should NOT trigger team-mode on '스팀으로' (false-positive guard)", async () => {
// given - text contains '팀으로' as substring of another Korean word ('스팀으로')
const collector = new ContextCollector()
const sessionID = "false-positive-eulo-session"
getMainSessionSpy = spyOn(sessionState, "getMainSessionID").mockReturnValue(sessionID)
const hook = createKeywordDetectorHook(createMockPluginInput(), collector)
const output = {
message: {} as Record<string, unknown>,
parts: [{ type: "text", text: "스팀으로 게임 켜줘" }],
}
// when - keyword detection runs
await hook["chat.message"]({ sessionID }, output)
// then - team-mode should NOT be triggered, text unchanged
const textPart = output.parts.find(p => p.type === "text")
expect(textPart).toBeDefined()
expect(textPart!.text).toBe("스팀으로 게임 켜줘")
expect(textPart!.text).not.toContain("[team-mode]")
})
test("should NOT trigger team-mode on '스팀모드' (Hangul-prefix false-positive guard)", async () => {
// given - text contains '팀모드' as substring of another Korean word ('스팀모드')
const collector = new ContextCollector()
const sessionID = "false-positive-mode-session"
getMainSessionSpy = spyOn(sessionState, "getMainSessionID").mockReturnValue(sessionID)
const hook = createKeywordDetectorHook(createMockPluginInput(), collector)
const output = {
message: {} as Record<string, unknown>,
parts: [{ type: "text", text: "스팀모드 활성화" }],
}
// when - keyword detection runs
await hook["chat.message"]({ sessionID }, output)
// then - team-mode should NOT be triggered
const textPart = output.parts.find(p => p.type === "text")
expect(textPart).toBeDefined()
expect(textPart!.text).toBe("스팀모드 활성화")
expect(textPart!.text).not.toContain("[team-mode]")
})
test("should NOT trigger team-mode on bare 'team' without 'mode'", async () => {
// given - text contains 'team' but not 'team mode'
const collector = new ContextCollector()
+3 -9
View File
@@ -2,16 +2,10 @@
* Team mode keyword detector.
*
* Triggers when the user explicitly invokes team-mode work:
* - English: team mode, team-mode, team_mode, teammode (case-insensitive)
* - Korean: 팀 모드, 팀모드, 팀으로
*
* The Korean variants use a negative lookbehind on Hangul syllables (가-힣)
* to prevent false positives like "스팀으로" matching "팀으로", or
* "스팀모드" matching "팀모드".
* team mode, team-mode, team_mode, teammode (case-insensitive)
*/
export const TEAM_PATTERN =
/\bteam[\s_-]?mode\b|(?<![가-힣])(?:팀\s*모드|팀으로)/i
export const TEAM_PATTERN = /\bteam[\s_-]?mode\b/i
export const TEAM_MESSAGE = `[team-mode]
Team mode reference detected. If user wants team-mode work, MUST orchestrate via team_* tools (team_create -> team_task_create + team_send_message). NEVER substitute with delegate_task - it is not equivalent. If team_* tools are unavailable (team_mode disabled in config), instruct user to set team_mode.enabled=true and restart opencode.`
Team-mode reference detected. Orchestrate via team_* tools (team_create -> team_task_create + team_send_message); NEVER substitute with delegate_task it is not equivalent. After every team_task_update that completes or fails a task, re-check team_task_list: if every task is terminal, run the closure sequence (team_shutdown_request + team_approve_shutdown per active member, then team_delete) in the same turn. Closing the team is the lead's responsibility, not the user's. If the team_* tools are absent, team_mode is disabled — tell the user to set team_mode.enabled=true and restart opencode.`
+3 -4
View File
@@ -100,10 +100,9 @@ function latestUserMessageRequestsTeamMode(
function buildTeamModeStatusContent(): string {
return `${TEAM_MODE_STATUS_MARKER}
Team mode is ENABLED for this session.
If the team_* tools are present, that is authoritative proof that team mode is active.
Do not inspect ~/.config/opencode or project config files to verify team mode.
If you need usage guidance, load the team-mode skill. Otherwise use the team_* tools directly.
Team mode is ENABLED for this session. Presence of the team_* tools is authoritative proof; do not inspect config files to verify.
Closure invariant: every team you open is yours to close. After each team_task_update that completes or fails a task, call team_task_list({ teamRunId }); if every task is terminal, run team_shutdown_request + team_approve_shutdown per active member, then team_delete — in the same turn, without waiting for the user to ask. Lingering teams are a defect.
Load the team-mode skill for the full Closure Contract and Closure Sequence.
</team_mode_status>`
}