Skip to content
This repository was archived by the owner on May 21, 2026. It is now read-only.

Commit 6eecaf8

Browse files
feat(agents): swarm phase 8 — orchestrator disposition + runtime guard
Closes a real UX gap surfaced in the user's live testing: the orchestrator (Gemini model) doesn't naturally reach for the `swarm` tool, even when registered, even with explicit prompt steering. It falls back to `run_shell_command` and tries `gemini swarm ...` recursively. The fork ships substantial swarm + policy machinery already (Phases 4–6), but the model-side disposition layer was missing. R1/R2 multi-model design debate (Opus + Gemini in parallel, two rounds) settled the design at `design-loop/swarm-orchestrator- disposition.md`. After implementation, a six-reviewer pass (three angles — TS/clean-code, intent, tests/abstractions — each with one Opus + one Gemini reviewer) ran in parallel; all six returned SHIP or SHIP-WITH-FOLLOW-UP. Cheap follow-ups are addressed in this commit; the rest are noted in the design doc and the agent-memory entry for v2 pickup. Four layers, all gated on `Config.isSwarmEnabled()` so upstream non-swarm users see zero behavior change: 1) Tool description rewrite (~+150 tokens). `SWARM_TOOL_DESCRIPTION` and `SWARM_STATUS_TOOL_DESCRIPTION` now carry explicit "when to use" + the anti-pattern "DO NOT shell out to `gemini` — there is no `gemini swarm` verb." Drift-guard tests pin the anti-pattern phrasing so accidental rewording regresses behavior loudly. 2) Orchestrator system-prompt disposition block + single inline worked example (~+300 tokens, conditional). New `SwarmDispositionOptions` + `renderSwarmDisposition` in `prompts/snippets.ts`. Inserted between `renderSubAgents` and `renderAgentSkills` in `getCoreSystemPrompt`. `promptProvider.ts` gates on `(SWARM_TOOL_NAME ∨ SWARM_STATUS_TOOL_NAME) ∈ registered tools` AND `isSwarmEnabled()`. The block names the tool, flags the no-CLI-verb anti-pattern, references `swarm_status`, and embeds one `<example>` showing a `swarm spawn` call. The example is inline rather than separate so in-context proximity isn't diluted by intervening prompt sections. 3) `swarm-collaboration` skill auto-inline (~+1.6k tokens, only when swarm enabled). `promptProvider.ts` filters the skill out of the regular `<available_skills>` manifest and inlines its SKILL.md body via new `renderSwarmInline`. Removes the activate-skill indirection so the protocol (state.md convention, release rules, paste-verbatim discipline) lands in the orchestrator's context without an extra round-trip. Hardcoded for the single fork-builtin inline candidate; an `inline: boolean` metadata flag was explicitly rejected in R2 (YAGNI; the LOCKED v2 north star reserves the capability/ template data-model surface). 4) Runtime guard — tier-1 default-deny `PolicyRule`. Registered from `config.ts` inside the existing `isSwarmEnabled()` tool- registration block. Pattern matches the JSON-stringified args form (`stableStringify(toolCall.args)`), NOT raw shell text — that ground-truth correction was caught in R2 when both models independently traced `PolicyEngine.matchRule` and found the raw-shell prototype from R1 would silently never match. The correct shape is `/"command":"(gemini|gemini-fork)(\s|"|\\)/`; the policy engine's existing sub-command splitter (`policy-engine.ts:469-475`) handles `bash -c "gemini foo"` and `cd /tmp && gemini ...` via recursive `check()`. The deny message carries the redirect text. `tier-4` user policies can still override. Review-cycle fixes folded in before commit: - Legacy snippets gap (Opus angle 1 #1): `snippets.legacy.ts` now imports the Phase 8 option types and renderers from `snippets.ts` and wires them into legacy `getCoreSystemPrompt`, so Gemini 2.x orchestrators with swarm enabled get the disposition block too. The disposition fix matters more for older models with stronger shell-first priors, not less. - Misleading test narration (Opus angles 2 + 3, cross-flagged): `policy-engine.test.ts` comment for the `bash -c "gemini help"` case had the wrong mechanism. The DENY actually fires via the sub-command splitter recursing into the inner `gemini help`, not the JSON-escape `(\\)` alternative at the top level. Corrected. - `swarm_status`-only branch test gap (Opus angle 3): added a case proving the disposition AND auto-inline both fire when only `SWARM_STATUS_TOOL_NAME` is registered (sub-agents themselves get `swarm_status` without `swarm` per the recursion-guard filter in `swarm-manager.ts`). - Empty-body edge case (Gemini angle 3): `renderSwarmInline` now returns `''` for whitespace-only bodies so a future skill loader returning an empty string doesn't render a dangling `# Skill — <name> (auto-loaded)` header with no content. Test added. Non-blocking review findings deferred to follow-ups (recorded in the design doc / agent-memory entry): - `SWARM_TOOL_NAME` SoT split (Opus angle 1 #3, Gemini angle 1): `SWARM_STATUS_TOOL_NAME` lives in lightweight `agents/swarm/ types.ts`, while `SWARM_TOOL_NAME` is still in heavy `swarm-tool.ts`. Consolidating both into the central `tools/tool-names.ts` is a separate cleanup. - Phase 8 prompt fields bypass `withSection` (Opus angle 1 #2): the operator `GEMINI_PROMPT_<KEY>=0` mute knob doesn't apply to the new sections. Routing through `withSection` is a separate consistency fix. - Layer 4 brittleness on `stableStringify` (Gemini angle 2): the argsPattern coupling to the policy engine's stringify format is a structural smell. A dedicated structured-arg matcher in the policy engine is a v2-level improvement. Tests landed (11 new): - `policy-engine.test.ts`: JSON-shape pattern denies `gemini …`, `gemini-fork …`, and `bash -c "gemini help"` (via recursive splitter); passes through `ls -la` and `echo gemini`. - `swarm-tool.test.ts`: anti-pattern drift guards on both tool descriptions. - `promptProvider.test.ts`: disposition block on/off; auto- inline pulled out/in manifest; only-`swarm_status` branch fires both layers; `renderSwarmInline` empty-body returns ''. - `config.test.ts`: tier-1 deny rule registered iff swarm enabled; carries the expected `source`, `toolName`, `decision`, `argsPattern` shape, and `denyMessage`. Gates: npm run typecheck — pass node scripts/lint.js --eslint — pass node scripts/lint.js --prettier — pass for staged files; design-loop docs auto-formatted by pre-commit hook npm run build --workspace=packages/core — pass npm run build --workspace=packages/cli — pass npx vitest run packages/core/src/policy/ — 330/332 (the 2 pre-existing topic-policy.test.ts failures, unrelated, verified on parent commit f190547) npx vitest run packages/core/src/agents/swarm/ — 36/36 npx vitest run packages/core/src/prompts/ — 65/65 Files committed include the LOCKED design doc (`design-loop/swarm-orchestrator-disposition.md`) and the `.gitignore` exception that tracks it (alongside `swarm-north-star.md`). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
1 parent d58be6e commit 6eecaf8

14 files changed

Lines changed: 770 additions & 25 deletions

‎.gitignore‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,9 @@ temp_agents/
6969
# conductor extension and planning directories
7070
conductor/
7171

72-
# Design-loop discussion artifacts (R-round briefings, model responses)
73-
# Only swarm-north-star.md is tracked (force-added).
72+
# Design-loop discussion artifacts (R-round briefings, model responses).
73+
# LOCKED architectural references are tracked; R-round discussions stay
74+
# local-only. Add new LOCKED docs as exceptions when they're produced.
7475
design-loop/*
7576
!design-loop/swarm-north-star.md
77+
!design-loop/swarm-orchestrator-disposition.md

‎CLAUDE.md‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,21 @@ subagent).
322322
The `/audit <agent_id>` slash command renders the effective policy + recent
323323
activity for one live swarm sub-agent.
324324

325+
### Orchestrator disposition (Phase 8 v1.x)
326+
327+
When `experimental.swarm: true`, the orchestrator's system prompt gains a
328+
dedicated "Swarm (experimental, enabled)" section with usage guidance,
329+
anti-pattern callouts (no `gemini swarm` CLI verb), and an inline `<example>`.
330+
The `swarm-collaboration` SKILL.md is also auto-inlined into the prompt rather
331+
than left in the activate-skill manifest, so the orchestrator sees the protocol
332+
without an extra indirection step.
333+
334+
Defense in depth: a tier-1 default-deny `PolicyRule` blocks `run_shell_command`
335+
invocations whose `command` arg starts with `gemini` or `gemini-fork`,
336+
redirecting to the in-process `swarm` tool. tier-4 user policy can override.
337+
Full rationale + R1/R2 ground-truth correction on the JSON-shape `argsPattern`
338+
lives in `design-loop/swarm-orchestrator-disposition.md`.
339+
325340
### P1 safety caps (v1.x)
326341

327342
- `max_turns` on `spawn` is capped at 50.

‎design-loop/swarm-north-star.md‎

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -168,14 +168,21 @@ R4 의 `request_capability` 도구는 v2 에서 부활 — 단순히 "orchestrat
168168

169169
## 로드맵 (3 milestones)
170170

171-
### v1.x — Scope Bridge (다음 커밋 범위)
171+
### v1.x — Scope Bridge (Phase 6 — shipped)
172172

173173
- `swarm spawn` 이 `policy: PolicyRule[]` 받음 (`tools: string[]` 과 공존, 양쪽
174-
허용 — back-compat)
175-
- 매 sub-agent `tool_use` 가 `PolicyEngine.check()` 통과 (subagent 필드 활용)
176-
- 사이드카 `swarm-policy.toml` (tier 2 기본값, `subagent=*`)
177-
- `/audit <agent_id>` 명령 ship
178-
- `swarm_status.effective_policy_summary` 확장
174+
허용 — back-compat). 각 rule 은 매니저가 `subagent`/`source`/`priority` 를
175+
강제 — tier-2 (EXTENSION_POLICY_TIER) band 안에 들어가서 user/admin 천장 아래.
176+
- 매 sub-agent `tool_use` 가 `PolicyEngine.check(subagent=agent_id)` 통과
177+
(scheduler 가 이미 threading).
178+
- 사이드카 `<repo>/.gemini/swarm-policy.toml` — workspace 정책 로딩과 같이
179+
들어와서 **tier 3 (WORKSPACE_POLICY_TIER)** 으로 등록. `subagent` 없는 rule 은
180+
자동으로 `'*'` 와일드카드 스탬핑 (`PolicyEngine.matchRule` 가 `'*'` 를
181+
"subagent 가 비어있지 않은 모든 caller" 매치로 처리하므로 orchestrator 는 영향
182+
안 받음).
183+
- `/audit <agent_id>` 명령 ship.
184+
- `swarm_status.effective_policy_summary` (counts allow/deny/ask_user + top
185+
rules by priority) 확장.
179186

180187
### v2 — north-star spine
181188

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
# Phase 8 — Swarm orchestrator-disposition architecture
2+
3+
**Status:** LOCKED 2026-05-19 — R1/R2 multi-model debate + user decision. v1.x
4+
scope. Sits on top of `swarm-north-star.md` (LOCKED 2026-05-17); must not
5+
violate the v2/v3 reservations there.
6+
7+
## 풀려는 문제
8+
9+
사용자 라이브 테스트로 확인된 UX 갭: orchestrator (Gemini 모델) 가 `swarm`
10+
도구가 등록되어 있어도 자연스럽게 안 잡고, "use swarm" 명시 지시 에도
11+
`run_shell_command` 로 `gemini swarm ...` 재귀 invocation 시도. R1/R2 에서 4겹
12+
갭으로 분해:
13+
14+
1. Gemini training prior: `swarm` 예시 0, shell 예시 ∞.
15+
2. Tool description 의 "when to use vs shell" 부재.
16+
3. Skill discovery 2단계 indirection (`activate_skill` 필요).
17+
4. Orchestrator system prompt 에 swarm-specific disposition 없음.
18+
19+
## 해결 — 4겹 보강
20+
21+
| # | 영역 | 메커니즘 | 비용 |
22+
| --- | ----------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | -------------------- |
23+
| 1 | Tool description rewrite | "when/not + no `gemini swarm` verb" 명시. `swarm-tool.ts:154-160`, `swarm-status-tool.ts:52-57` | ~+150 토큰 |
24+
| 2 | Orchestrator disposition + worked example | `snippets.ts` 에 `renderSwarmDisposition` 신설, 본문에 single `<example>` inline. `swarm` 도구 등록 + `isSwarmEnabled()` 시 활성 | ~+300 토큰 (조건부) |
25+
| 3 | swarm-collaboration skill auto-inline | `promptProvider.ts:166-175` 에서 manifest 에서 빼고 본문 inline. `isSwarmEnabled()` 시에만 | ~+1.6k 토큰 (조건부) |
26+
| 4 | 런타임 deny rule | tier-1 PolicyRule, `argsPattern` 는 JSON-shape (stableStringify) — `/"command":"(gemini\|gemini-fork)(\s\|"\|\\\\)/` | 0 토큰 (런타임) |
27+
28+
활성화 게이트: 모두 `Config.isSwarmEnabled()`. upstream gemini 사용자 영향 0.
29+
30+
## 핵심 결정
31+
32+
### Q1 — Tool description rewrite
33+
34+
`SWARM_TOOL_DESCRIPTION` (현재 ~80자, "what" 만):
35+
36+
> "Manage a persistent swarm of long-lived Claude sub-agents. Use
37+
> `action: spawn` ..."
38+
39+
→ 새 (~480자, "when + not + no-CLI 명시"):
40+
41+
> "Spawn and message persistent Claude sub-agents in-process. Use for
42+
> parallel/multi-perspective work, isolating sub-tasks, or specialist roles
43+
> (reviewer, doc-writer). DO NOT shell out to `gemini` — there is no
44+
> `gemini swarm` verb; this in-process tool IS the swarm. Actions: spawn
45+
> (returns agent_id), message (stateful), release, list."
46+
47+
`SWARM_STATUS_TOOL_DESCRIPTION` 도 동일 정신, "Read-only. Call at start of any
48+
non-trivial swarm task." prepend.
49+
50+
### Q2 — Skill auto-inline 메커니즘: **hardcode**
51+
52+
`promptProvider.ts:166-175` 에서 `isSwarmEnabled()` 이면:
53+
54+
1. `swarm-collaboration` skill 을 manifest 리스트에서 제외
55+
2. 그 body 를 새 옵션 `swarmInline` 으로 `getCoreSystemPrompt` 에 전달
56+
3. `snippets.ts` 의 새 `renderSwarmInline` 가 본문 inline (이걸
57+
`renderAgentSkills` 직전에 둠)
58+
59+
**`SkillDefinition.metadata.inline` 같은 플래그 도입 거부.** 이유:
60+
61+
- v1.x inline 후보는 정확히 1개 (`swarm-collaboration`)
62+
- R5 v2 의 `CapabilityTemplate` 가 별도 데이터 모델로 들어올 예정이라 플래그가
63+
v2 와 reconcile 필요해질 위험
64+
- YAGNI — 두 번째 inline 후보 나오면 그때 metadata 로 리팩토링
65+
66+
### Q3 — Disposition block + worked example: **결합**
67+
68+
새 `renderSwarmDisposition` 함수:
69+
70+
- `snippets.ts:43` `SystemPromptOptions` 에
71+
`swarmDisposition?: SwarmDispositionOptions` 추가
72+
- `snippets.ts:144` 의 system prompt assembly 에서 `renderSubAgents` 와
73+
`renderAgentSkills` **사이** 에 삽입
74+
- `promptProvider.ts:166-175` 에서
75+
`enabledToolNames.has(SWARM_TOOL_NAME) && config.isSwarmEnabled()` 일 때 옵션
76+
set
77+
78+
블록 prototype (~600자):
79+
80+
```
81+
# Swarm (experimental, enabled)
82+
83+
You have a `swarm` tool for long-lived Claude sub-agents in-process.
84+
Use for parallel independent work, multi-perspective review, or
85+
isolating noisy sub-tasks. Do NOT invoke `gemini`/`gemini-fork` via
86+
`run_shell_command` — there is no CLI verb; the in-process tool IS
87+
the mechanism. Call `swarm_status()` before non-trivial swarm work.
88+
Skip swarm for single-target lookups (use read_file/grep directly).
89+
90+
<example>
91+
user: review my diff from two angles
92+
assistant: <thinking>The user wants multi-perspective review. Use the
93+
swarm tool to spawn parallel reviewers.</thinking>
94+
swarm({
95+
action: 'spawn', model: 'sonnet',
96+
role: 'correctness-reviewer',
97+
charter: 'verify behavior',
98+
system_prompt: '...'
99+
})
100+
</example>
101+
```
102+
103+
example 분리 거부: 단일 conceptual block 가 in-context proximity 보장.
104+
105+
### Q4 — Subagent archetype menu: **deferred to v2**
106+
107+
R5 north star v2 의 `CapabilityTemplate` 가 정식 메뉴 시스템. v1.x 에 임시 메뉴
108+
박으면 v2 와 conflict. 단, `.gemini/skills/swarm-collaboration/SKILL.md` 본문
109+
(Q3 inline 대상) 에 1줄 추가: "Common roles: reviewer, refactorer, doc-writer,
110+
researcher, devil's-advocate". skill body 는 content (자유롭게 재작성 가능),
111+
tool description 은 contract (메뉴 박으면 v2 baggage).
112+
113+
### Q5 — 런타임 deny rule: **tier-1 PolicyEngine, JSON-shape pattern**
114+
115+
`config.ts:4159` (swarm tool 등록 직후, `isSwarmEnabled()` 블록 안):
116+
117+
```ts
118+
this.policyEngine.addRule({
119+
toolName: 'run_shell_command',
120+
argsPattern: /"command":"(gemini|gemini-fork)(\s|"|\\)/,
121+
decision: PolicyDecision.DENY,
122+
priority: DEFAULT_POLICY_TIER, // = 1
123+
source: 'swarm-recursive-guard',
124+
denyMessage:
125+
'Do not invoke gemini recursively. Use the in-process `swarm` ' +
126+
'tool (action: spawn).',
127+
});
128+
```
129+
130+
**Ground-truth 정정 (R2 양쪽 모두 확인):**
131+
132+
- `PolicyEngine.matchRule` 의 `argsPattern` 은
133+
`RegExp.test(stableStringify(toolCall.args))`
134+
- `stableStringify` 가 JSON 형태로 변환 + 키 사이 null byte delimiter
135+
(`stable-stringify.ts:128-132`)
136+
- R1 의 raw shell pattern `^\s*(gemini|gemini-fork)\b` 는 **절대 매치 안 함**
137+
- 올바른 매칭: JSON-quoted form `"command":"gemini ..."` 종결문자 `\s`/`"`/`\`
138+
중 하나
139+
140+
Sub-command 분할 (`policy-engine.ts:469-475`) 가 `bash -c 'gemini foo'`,
141+
`cd /tmp && gemini help` 등도 재귀 `check()` 로 처리해서 같은 룰에 걸림.
142+
143+
**shell-tool 직접 deny 거부.** 이유:
144+
145+
- `removeRulesByTier`, `/audit`, tier-4 user override 우회
146+
- R5 north star 의 "런타임 = PolicyEngine" 모델과 분리
147+
- 단순성 < 일관성
148+
149+
tier-4 override 보존: 사용자가 `~/.gemini/policies/*.toml` 에서 같은 패턴 ALLOW
150+
박으면 천장이 이김.
151+
152+
### Q6 — Worked example: **single, inside Q3 block** (위 참조)
153+
154+
## 테스트 요구사항
155+
156+
1. **PolicyEngine pattern**: `policy-engine.test.ts` 에 case 추가 —
157+
`argsPattern: /"command":"gemini.../` 가 `{command:'gemini --version'}` AND
158+
`{command:'bash -c "gemini help"'}` 둘 다 DENY 처리. JSON-shape 매칭
159+
invariant lock.
160+
2. **Tool description**: `swarm-tool.test.ts` 에서 description 에 "no
161+
`gemini swarm` verb" 같은 핵심 phrase 존재 확인 (drift guard).
162+
3. **Disposition block**: `promptProvider.test.ts` 에 case — swarm enabled 시
163+
`renderSwarmDisposition` 출력이 시스템 프롬프트에 포함, disabled 시 불포함.
164+
4. **Skill auto-inline**: `promptProvider.test.ts` 에 case — swarm enabled 시
165+
`swarm-collaboration` 이 manifest 에서 빠지고 body 가 inline 됨, disabled 시
166+
manifest 그대로.
167+
5. **Recursive guard end-to-end**: `swarm-recursive-guard.test.ts` 신설 (또는
168+
swarm-manager.test.ts 확장) — `isSwarmEnabled()` 시 orchestrator 가
169+
`run_shell_command{command:'gemini ...'}` 호출하면 DENY + denyMessage.
170+
171+
## Out of scope
172+
173+
- swarm 권한 모델 (R5 north star v1.x 가 이미 처리; Phase 8 은 disposition layer
174+
만)
175+
- `CapabilityTemplate` (v2)
176+
- `amend_policy` action (v2)
177+
- 시스템 reminder 인젝션 (Claude Code 의 Pattern F — Gemini 런타임에 동등
178+
surface 없음)
179+
- 사용자-author skill 의 metadata 기반 inline (v2 이후 검토)
180+
181+
## Roadmap 위치
182+
183+
```
184+
v1.0 baseline (Phase 5 = ab572fa3c)
185+
v1.0.1 (Phase 5.1 fixes = cbfa37206)
186+
v1.x Scope Bridge (Phase 6 = c07139ea3) — PolicyRule[] capability
187+
v1.x Disposition (Phase 8, this doc) — orchestrator disposition + runtime guard
188+
↓
189+
v2 north-star spine (CapabilityTemplate, amend_policy, BehaviorConfig)
190+
v3 agent unification
191+
```
192+
193+
## 변경 절차
194+
195+
이 LOCKED 문서 수정은 swarm-north-star.md 와 동일 절차:
196+
197+
1. 새 R-round 문서 추가
198+
2. 2개 model 의견
199+
3. 사용자 명시 승인
200+
4. LOCKED 일자 갱신

‎packages/core/src/agents/swarm/swarm-status-tool.ts‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,16 @@ import { SWARM_STATUS_TOOL_NAME } from './types.js';
4949
export { SWARM_STATUS_TOOL_NAME };
5050
export const SWARM_STATUS_TOOL_DISPLAY_NAME = 'Swarm Status';
5151

52-
const SWARM_STATUS_TOOL_DESCRIPTION =
53-
'Return a read-only snapshot of the current swarm: every live ' +
54-
'sub-agent (with role/charter), the shared workspace directory path, ' +
55-
'and up to 50 most-recent spawn/message/release events (newest ' +
56-
'first). Sub-agents should call this at the start of any non-trivial ' +
57-
'task so they know who else is on the team.';
52+
// Phase 8 — prefix the orchestrator-side usage hint ("Read-only. Call at
53+
// the start of any non-trivial swarm task.") so the model gets the
54+
// when-to-use signal directly in the tool description.
55+
export const SWARM_STATUS_TOOL_DESCRIPTION =
56+
'Read-only. Call at the start of any non-trivial swarm task. ' +
57+
'Returns a snapshot of the current swarm: every live sub-agent ' +
58+
'(with role/charter), the shared workspace directory path, and ' +
59+
'up to 50 most-recent spawn/message/release events (newest ' +
60+
'first). Sub-agents should also call this at the start of any ' +
61+
'non-trivial task so they know who else is on the team.';
5862

5963
/**
6064
* Empty JSON schema — `swarm_status` takes no arguments. We keep the

‎packages/core/src/agents/swarm/swarm-tool.test.ts‎

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,12 @@
2121

2222
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2323
import { EventEmitter } from 'node:events';
24-
import { SwarmTool, SWARM_TOOL_NAME } from './swarm-tool.js';
24+
import {
25+
SwarmTool,
26+
SWARM_TOOL_NAME,
27+
SWARM_TOOL_DESCRIPTION,
28+
} from './swarm-tool.js';
29+
import { SWARM_STATUS_TOOL_DESCRIPTION } from './swarm-status-tool.js';
2530
import { SwarmManager } from './swarm-manager.js';
2631
import { SubagentState, type SubagentProgress } from '../types.js';
2732
import { Kind } from '../../tools/tools.js';
@@ -301,4 +306,29 @@ describe('SwarmInvocation — Phase 5.1 progress streaming', () => {
301306
expect(tool.name).toBe(SWARM_TOOL_NAME);
302307
expect(SWARM_TOOL_NAME).toBe('swarm');
303308
});
309+
310+
// Phase 8 — drift guard on the orchestrator-disposition phrasing.
311+
// The tool description is the orchestrator's first signal (Gemini's
312+
// training prior is shell-heavy); the rewritten description gives it
313+
// explicit "when to use / not to shell out" guidance plus an anti-
314+
// pattern callout against recursive `gemini swarm` CLI invocation.
315+
// Accidental removal of either phrase regresses Phase 8 disposition.
316+
// See `design-loop/swarm-orchestrator-disposition.md` Q1.
317+
it('Phase 8 — SWARM_TOOL_DESCRIPTION carries the no-CLI-verb anti-pattern', () => {
318+
expect(SWARM_TOOL_DESCRIPTION).toMatch(/no `gemini swarm` verb/);
319+
expect(SWARM_TOOL_DESCRIPTION).toMatch(/DO NOT shell out/);
320+
// The "in-process" framing differentiates swarm from out-of-process
321+
// patterns (the `async-pr-review` skill). Lock that too.
322+
expect(SWARM_TOOL_DESCRIPTION).toMatch(/in-process/);
323+
});
324+
325+
// Companion drift guard on the status-tool description prefix. Phase 8
326+
// prepends "Read-only. Call at the start of any non-trivial swarm
327+
// task." so the orchestrator gets a usage-time hint about when to call
328+
// swarm_status (as the very first action of any swarm interaction).
329+
it('Phase 8 — SWARM_STATUS_TOOL_DESCRIPTION starts with the "Read-only / call at start" usage hint', () => {
330+
expect(SWARM_STATUS_TOOL_DESCRIPTION).toMatch(
331+
/^Read-only\. Call at the start of any non-trivial swarm task\./,
332+
);
333+
});
304334
});

‎packages/core/src/agents/swarm/swarm-tool.ts‎

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -151,13 +151,19 @@ const SWARM_JSON_SCHEMA = {
151151
],
152152
} as const;
153153

154-
const SWARM_TOOL_DESCRIPTION =
155-
'Manage a persistent swarm of long-lived Claude sub-agents. Use ' +
156-
'`action: "spawn"` to create a new session (returns an agent_id), ' +
157-
'`action: "message"` to send a prompt to an existing session (session ' +
158-
'state is retained across calls), `action: "release"` to terminate a ' +
159-
'session, and `action: "list"` to enumerate live sessions. Sessions are ' +
160-
'in-memory and bound to the parent CLI process lifetime.';
154+
// Phase 8 — orchestrator disposition layer. The Gemini orchestrator
155+
// over-trusts shell and under-uses this tool by default; the description
156+
// has to do double duty as "what" + "when" + an explicit anti-pattern
157+
// callout against recursive `gemini` CLI invocation. See
158+
// `design-loop/swarm-orchestrator-disposition.md` Q1.
159+
export const SWARM_TOOL_DESCRIPTION =
160+
'Spawn and message persistent Claude sub-agents in-process. Use ' +
161+
'for parallel/multi-perspective work, isolating sub-tasks, or ' +
162+
'specialist roles (reviewer, doc-writer). DO NOT shell out to ' +
163+
'`gemini` — there is no `gemini swarm` verb; this in-process tool ' +
164+
'IS the swarm. Actions: `spawn` (returns agent_id), `message` ' +
165+
'(stateful turn on an existing session), `release`, `list`, ' +
166+
'`amend_policy`. Pair with `swarm_status` for the live view.';
161167

162168
/**
163169
* The `swarm` declarative tool. Phase 1: validates params and returns a

0 commit comments

Comments
 (0)