Skip to content

Commit 112c5b4

Browse files
committed
fix: 收紧 TeamCreate 误触发与重复创建
- 阻止非显式持续协作请求误入 TeamCreate\n- 阻止占位或保留 team 名污染原生 team 创建\n- 阻止已验证 active team 的同名重复创建\n- 收紧 session 级 team bootstrap 文案\n- 补充普通实现与 TeamCreate 临界场景回归测试
1 parent 2477545 commit 112c5b4

8 files changed

Lines changed: 146 additions & 6 deletions

.claude-plugin/marketplace.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
{
1111
"name": "hello2cc",
1212
"description": "Thin Claude Code plugin for third-party models, focused on host state, protocol adaptation, and repeated-failure debounce.",
13-
"version": "0.5.4",
13+
"version": "0.5.5",
1414
"source": "./",
1515
"author": {
1616
"name": "hello2cc"

.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "hello2cc",
3-
"version": "0.5.4",
3+
"version": "0.5.5",
44
"description": "Claude Code alignment plugin that drives third-party models toward Opus-compatible workflows, tool usage, and output style.",
55
"author": {
66
"name": "hello2cc"

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# 更新日志
22

3+
## 0.5.5 - 2026-04-12
4+
5+
- 修复非显式持续协作请求下 `TeamCreate` 被误放行的问题,普通单次 worker / 一次性实现任务会在 `pre-team-create` 被拦回 plain `Agent` 路径
6+
- 新增 `TeamCreate.team_name` 占位值与保留 assistant team 名校验,阻断 `none` / `__omit__` / `main` 一类无效 team 名继续污染原生 team 创建链路
7+
- 当会话里已经存在已验证的 active team + task board 连续体时,阻止重复创建同名 team,避免偶发进入 Agent Team 后反复创建、反复失败
8+
- 收紧 capability policy 的 session 级 team 文案:只有当前请求明确要求持久 task board / owner / handoff / shared teammate context 时才引导走 real team bootstrap
9+
- 补充非 team 请求、占位 team 名、重复创建 active team、full team tools 下普通实现提示四类回归测试
10+
311
## 0.5.4 - 2026-04-12
412

513
- 统一在 session / team store、tool success/failure、TeammateIdle、route continuity 等入口净化 `none``__omit__` 一类伪 team 名,避免旧脏状态把普通 subagent 误判回 Agent Team

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "hello2cc",
3-
"version": "0.5.4",
3+
"version": "0.5.5",
44
"type": "module",
55
"description": "Claude Code alignment plugin that drives third-party models toward Opus-compatible workflows, tool usage, and output style.",
66
"license": "Apache-2.0",

scripts/lib/agent-input.mjs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,16 @@ import {
1010
wantsIntentWorktree,
1111
} from './agent-input-shared.mjs';
1212
import { participantNameOrEmpty } from './participant-name.mjs';
13+
import { realTeamNameOrEmpty } from './team-name.mjs';
1314

1415
function joinReasons(...items) {
1516
return items.filter(Boolean).join('; ');
1617
}
1718

19+
function sameCaseInsensitiveValue(left, right) {
20+
return readTrimmed(left).toLowerCase() === readTrimmed(right).toLowerCase();
21+
}
22+
1823
export function normalizeAgentTeamSemantics(input = {}, sessionContext = {}) {
1924
const rawWorkerName = readTrimmed(input?.name);
2025
const workerName = participantNameOrEmpty(rawWorkerName);
@@ -125,6 +130,42 @@ export function normalizeAgentIsolation(input = {}, sessionContext = {}) {
125130
}
126131

127132
export function normalizeTeamCreateInput(input = {}, sessionContext = {}) {
133+
const rawRequestedTeamName = readTrimmed(input?.team_name);
134+
const requestedTeamName = realTeamNameOrEmpty(rawRequestedTeamName);
135+
const activeTeamName = realTeamNameOrEmpty(sessionContext?.teamName);
136+
const teamSemantics = hasIntentTeamSemantics(sessionContext);
137+
138+
if (rawRequestedTeamName && !requestedTeamName) {
139+
return {
140+
input,
141+
changed: false,
142+
blocked: true,
143+
reason: `hello2cc blocked TeamCreate.team_name=${JSON.stringify(rawRequestedTeamName)} because placeholder or reserved assistant team names cannot create a real native team; use a concrete team_name instead`,
144+
};
145+
}
146+
147+
if (!teamSemantics) {
148+
return {
149+
input,
150+
changed: false,
151+
blocked: true,
152+
reason: 'hello2cc blocked TeamCreate because the current request does not imply sustained team semantics; plain workers should stay on Agent without creating a native team',
153+
};
154+
}
155+
156+
if (
157+
activeTeamName
158+
&& provenActiveTeamContext(sessionContext)
159+
&& (!requestedTeamName || sameCaseInsensitiveValue(requestedTeamName, activeTeamName))
160+
) {
161+
return {
162+
input,
163+
changed: false,
164+
blocked: true,
165+
reason: `hello2cc blocked redundant TeamCreate because a verified active team context already exists (${activeTeamName}); continue via SendMessage, task board tools, and named Agent teammates instead of recreating the same team`,
166+
};
167+
}
168+
128169
return { input, changed: false, reason: '', blocked: false };
129170
}
130171

scripts/lib/capability-policy-execution-definitions.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export const EXECUTION_POLICY_DEFINITIONS = [
2727
if (activeTeamName(sessionContext)) {
2828
lines.push('- 当前已处于真实 active team continuity;继续沿 `SendMessage` 与 task board 工具收口,不要把团队状态退化回正文口头广播。');
2929
} else if (hasBootstrappableTeamWorkflowSurface(sessionContext)) {
30-
lines.push('- 进入 team 模式后,先 `TeamCreate`,再 `TaskList` / `TaskCreate` 建真实 task board,再启动 teammate;后续 `Agent` 显式传 `name` + `team_name`。');
30+
lines.push('- 当前宿主具备真实 team bootstrap 能力,但只有当前请求明确要求持久 task board / owner / handoff / shared teammate context 时才启用;否则继续 plain `Agent` worker。');
3131
} else {
3232
lines.push('- 当前未看到完整的 team bootstrapping 工具面;不要把普通 `Agent` worker、background agents,或暂时出现的 teammate UI 误读成真实 team 已创建。');
3333
}

tests/orchestrator-agent-team.test.mjs

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ test('pre-agent-model strips reserved assistant team placeholders', () => {
174174
assert.match(output.hookSpecificOutput.permissionDecisionReason, /implicit assistant team semantics/i);
175175
});
176176

177-
test('pre-team-create no longer denies team creation when the request does not imply sustained team semantics', () => {
177+
test('pre-team-create denies team creation when the request does not imply sustained team semantics', () => {
178178
const env = isolatedEnv();
179179

180180
run('route', {
@@ -191,7 +191,8 @@ test('pre-team-create no longer denies team creation when the request does not i
191191
},
192192
}, env);
193193

194-
assert.deepEqual(output, { suppressOutput: true });
194+
assert.equal(output.hookSpecificOutput.permissionDecision, 'deny');
195+
assert.match(output.hookSpecificOutput.permissionDecisionReason, /does not imply sustained team semantics/i);
195196
});
196197

197198
test('pre-team-create allows native team creation for sustained collaboration requests', () => {
@@ -214,6 +215,73 @@ test('pre-team-create allows native team creation for sustained collaboration re
214215
assert.deepEqual(output, { suppressOutput: true });
215216
});
216217

218+
test('pre-team-create blocks placeholder or reserved assistant team names', () => {
219+
const env = isolatedEnv();
220+
221+
run('route', {
222+
session_id: 'teamcreate-placeholder',
223+
prompt: 'Use TeamCreate with teammates and a shared task board for this work.',
224+
}, env);
225+
226+
const output = run('pre-team-create', {
227+
session_id: 'teamcreate-placeholder',
228+
tool_name: 'TeamCreate',
229+
tool_input: {
230+
team_name: '__omit__',
231+
description: 'Bad placeholder',
232+
},
233+
}, env);
234+
235+
assert.equal(output.hookSpecificOutput.permissionDecision, 'deny');
236+
assert.match(output.hookSpecificOutput.permissionDecisionReason, /placeholder or reserved assistant team names/i);
237+
});
238+
239+
test('pre-team-create blocks redundant creation of an already active verified team', () => {
240+
const env = isolatedEnv();
241+
const sessionId = 'teamcreate-active-redundant';
242+
243+
run('post-tool-use', {
244+
session_id: sessionId,
245+
tool_name: 'TeamCreate',
246+
tool_input: {
247+
team_name: 'delivery-squad',
248+
},
249+
tool_response: {
250+
team_name: 'delivery-squad',
251+
},
252+
}, env);
253+
run('post-tool-use', {
254+
session_id: sessionId,
255+
tool_name: 'TaskCreate',
256+
tool_input: {
257+
subject: 'Implement frontend slice',
258+
description: 'Real task board continuity exists now',
259+
},
260+
tool_response: {
261+
task: {
262+
id: '1',
263+
subject: 'Implement frontend slice',
264+
},
265+
},
266+
}, env);
267+
run('route', {
268+
session_id: sessionId,
269+
prompt: 'Continue coordinating teammates on the shared task board for this implementation.',
270+
}, env);
271+
272+
const output = run('pre-team-create', {
273+
session_id: sessionId,
274+
tool_name: 'TeamCreate',
275+
tool_input: {
276+
team_name: 'delivery-squad',
277+
description: 'Recreate existing team',
278+
},
279+
}, env);
280+
281+
assert.equal(output.hookSpecificOutput.permissionDecision, 'deny');
282+
assert.match(output.hookSpecificOutput.permissionDecisionReason, /verified active team context already exists/i);
283+
});
284+
217285
test('pre-enter-worktree no longer pre-denies worktree creation when the prompt did not explicitly request it', () => {
218286
const env = isolatedEnv();
219287

tests/orchestrator-route-execution.test.mjs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,29 @@ test('route treats repo-heavy forward-slash Windows paths as complex implementat
313313
assert.doesNotMatch(context, / team|TeamCreate/);
314314
});
315315

316+
test('route does not inject bootstrap team steps for plain implementation prompts even when the host exposes full team tools', () => {
317+
const env = isolatedEnv();
318+
run('session-start', {
319+
session_id: 'route-plain-impl-no-team-bootstrap',
320+
model: 'opus',
321+
tools: ['Agent', 'TeamCreate', 'TaskCreate', 'TaskList', 'TaskGet', 'TaskUpdate', 'SendMessage'],
322+
}, env);
323+
324+
const output = run('route', {
325+
session_id: 'route-plain-impl-no-team-bootstrap',
326+
tools: ['Agent', 'TeamCreate', 'TaskCreate', 'TaskList', 'TaskGet', 'TaskUpdate', 'SendMessage'],
327+
prompt: 'Implement a focused one-file fix in scripts/lib/orchestrator-commands.mjs and keep the normal path.',
328+
}, env);
329+
const context = output.hookSpecificOutput.additionalContext;
330+
const state = parseAdditionalContextJson(context);
331+
332+
assert.ok(state.host.tools.includes('TeamCreate'));
333+
assert.equal(state.intent.actions.implement, true);
334+
assert.equal(state.intent.routing.bounded_implementation, true);
335+
assert.doesNotMatch(context, / team `TeamCreate`/);
336+
assert.doesNotMatch(context, / `TeamCreate` `TaskList` \/ `TaskCreate` task board teammate/);
337+
});
338+
316339
test('route keeps protocol explanation prompts out of capability and team-status routing', () => {
317340
const env = isolatedEnv();
318341
const cases = [

0 commit comments

Comments
 (0)