fix(team-mode): make lead close teams on its own initiative

The lead currently leaves teams alive after the task list drains because
none of the prompt surfaces tell it WHEN to close or HOW. omx-style
'self-closing' behavior was missing for four reasons (diagnosed via
prompt-engineering A/B/C: wrong / misframed / missing):

1. builtin team-mode skill 'Lifecycle' (B+C): 'phase ends / shape
   outgrown' is qualitative, so the model maps it to 'wait for user'.
   Step 6 jumped to team_delete without the request/approve pair the
   tool contract requires. Replaced with a 'Closure Contract' (a
   computable predicate over team_task_list + team_status) and an
   explicit 'Closure Sequence' (request -> approve -> delete, with
   force=true reserved for unrecoverable paths only).

2. TEAM_MESSAGE keyword injection (C): spent 100%% of its one-shot
   budget on routing ('do not substitute delegate_task'), 0%% on
   closure. Added the same closure rule in compressed form. Kept the
   'NEVER substitute with delegate_task' literal that
   keyword-detector/index.test.ts depends on.

3. team-mode-status-injector body (C): the only per-session injection
   for team mode had no closure obligation. Replaced the optional
   'load the team-mode skill ... otherwise use the team_* tools'
   sentence with a 'Closure invariant' clause that ties the check to
   every team_task_update.

4. member-guidance Wrap-up (A+B): step 3 said 'so the lead can decide
   whether to request shutdown', but team_shutdown_request is
   lead-only - members cannot initiate it. Step ordering also placed
   the completion message before team_task_update, so the lead's
   closable check would see stale data. Reordered to
   task_update -> check task_list for new work -> if nothing left,
   send a single 'closure-ready' message and idle. Test assertion
   updated to match the new accurate contract.

Also: stripped Korean alternation from TEAM_PATTERN per directive
('절대로 코드 내에 한국어 적지 마라'). Pattern is now
/\\bteam[\\s_-]?mode\\b/i. Removed 4 Korean test cases
(2 positive triggers + 2 false-positive guards) that the pattern no
longer needs to defend, and updated the keyword-detector AGENTS.md
row.

Net: -71 lines across prompt surfaces. The Closure Contract is the
only addition; everything else tightened.

Tests: 428/428 pass across src/features/team-mode/,
src/features/builtin-skills/, src/hooks/keyword-detector/,
src/hooks/team-mode-status-injector/, src/hooks/team-mailbox-injector/,
src/hooks/team-tool-gating/, src/hooks/team-session-events/.
LSP: no errors introduced (one pre-existing error in
keyword-detector/index.test.ts confirmed pre-existing on dev).
This commit is contained in:
YeonGyu-Kim
2026-05-24 15:21:24 +09:00
parent 2bfad4909e
commit 5f1fb0c52a
7 changed files with 35 additions and 106 deletions
@@ -91,7 +91,7 @@ Do not use \`oracle\`, \`prometheus\`, or other non-eligible agents here. For th
## Lifecycle
Teams are **ephemeral**: one team per phase of work. The moment a phase ends, or the team's shape no longer fits the next problem, **call \`team_delete\` immediately and spawn a fresh team for the next phase**. There is no in-place reshape; restructuring is delete-then-create. Lingering teams burn sessions, mailbox quota, and member-turn budget.
Teams are **ephemeral**. There is no in-place reshape restructuring is delete-then-create. Lingering teams burn sessions, mailbox quota, and member-turn budget every idle minute.
One cycle:
@@ -99,8 +99,28 @@ One cycle:
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. A member that finishes early asks to leave with \`team_shutdown_request\`; the lead handles \`team_approve_shutdown\` or \`team_reject_shutdown\`.
6. **Phase done or shape outgrown? Call \`team_delete\` now; no idle members "just in case." Loop to step 1 for the next phase.**
5. When the **Closure Contract** below holds, the lead runs the **Closure Sequence** in the same turn. Loop to step 1 for the next phase.
### Closure Contract
A team is **closable** when ALL of the following hold, as observed by \`team_task_list({ teamRunId })\` and \`team_status({ teamRunId })\`:
- Every task is in a terminal state: \`completed\` or \`failed\`. (No \`pending\`, no \`claimed\`, no \`in_progress\`.)
- No outstanding \`team_shutdown_request\` is still awaiting approval.
- The user has not asked you to keep the team open for follow-up.
Closure is **the lead's responsibility**, not the user's. Do not wait to be told. The check runs after every \`team_task_update\` that completes or fails a task — if the contract holds, close in the same turn. Closure now is cheaper than closure after the next user message, because by then the model has paged out the context.
### Closure Sequence
Run in order:
1. For each active member \`M\` returned by \`team_status\`:
- \`team_shutdown_request({ teamRunId, memberName: M })\`
- \`team_approve_shutdown({ teamRunId, memberName: M })\`
2. \`team_delete({ teamRunId })\`
If step 2 errors because a member is still active, re-run \`team_status\`. Use \`team_delete({ teamRunId, force: true })\` **only** after confirming the remaining member is not mid-write — for example, after an unrecoverable error path where graceful shutdown is impossible. Do not use \`force: true\` to skip step 1.
## Task ownership
+4 -4
View File
@@ -38,9 +38,9 @@ Going idle after sending a message is the expected flow — it does NOT mean you
## Wrap-up
When you finish your assigned work, ALWAYS:
1. Send your results to the lead via \`team_send_message\`.
2. Mark your task as completed via \`team_task_update\`.
3. Send a completion message to the lead so the lead can decide whether to request shutdown.
When you finish your assigned work, ALWAYS, in this order:
1. Mark your task \`status: "completed"\` (or \`"failed"\` with a reason) via \`team_task_update\` — the lead's closure check reads \`team_task_list\`, so the task update must land before any completion message.
2. Re-check \`team_task_list\` for newly unblocked work. If there is any, claim it and continue — do not idle.
3. If \`team_task_list\` shows nothing left for you, send the lead a single short \`team_send_message\` with your results and the phrase \`closure-ready\` so the lead knows you have no more work in flight. Then go idle.
`
}
@@ -224,7 +224,7 @@ describe("createTeamRun", () => {
expect(firstPrompt).toContain("Include `summary` and `references`")
expect(firstPrompt).toContain("Move to `status: \"in_progress\"` when you start working")
expect(firstPrompt).toContain("Do NOT call this from inside team members")
expect(firstPrompt).toContain("lead can decide whether to request shutdown")
expect(firstPrompt).toContain("closure-ready")
expect(firstPrompt).toContain("user interacts primarily with the team lead")
expect(firstPrompt).toContain("Idle is normal")
expect(firstPrompt).toContain("structured JSON status messages")