diff --git a/src/adapters/cli/claude-code.ts b/src/adapters/cli/claude-code.ts index 188cb6401..a50ab92bd 100644 --- a/src/adapters/cli/claude-code.ts +++ b/src/adapters/cli/claude-code.ts @@ -1149,6 +1149,9 @@ export function createClaudeFamilyAdapter(variant: ClaudeFamilyVariant, rawBin: // `botmux session-ready` 给出启动 selector 边界。worker 收到后清掉旧 // readyPattern 证据,并等待新 prompt 再投首条消息。 injectsReadyHook: true, + // `/effort` 不在此处——它是全局 PASSTHROUGH_COMMANDS 的成员(所有 CLI 尽力透传, + // 且刻意不带冷启动语义)。这里只保留 `/goal`:它是「开启一段目标工作」的命令,需要 + // 空 topic 冷启动能力(isInitialSessionPassthrough 只认 adapter 层的这个字段)。 defaultPassthroughCommands: variant.id === 'claude-code' ? ['/goal'] : undefined, // Seed shares most of this adapter but has not been verified to expose the // same native session-rename command. Keep the capability exact to Claude. diff --git a/src/core/passthrough-commands.ts b/src/core/passthrough-commands.ts index e3c160ea9..df1a935c4 100644 --- a/src/core/passthrough-commands.ts +++ b/src/core/passthrough-commands.ts @@ -27,6 +27,16 @@ export const PASSTHROUGH_COMMANDS = new Set([ '/code-review', '/security-review', '/review', // Codex:/btw 向当前会话追加一条旁注/引导消息 '/btw', + // 推理强度调档。放全局(而非某个 adapter 的 defaultPassthroughCommands)是刻意的: + // ① 这里的命令本就是「尽力透传」——/plugin /mcp /btw 也并非所有 CLI 都支持, + // CLI 认得就生效、认不得顶多回一句 unknown-command(不崩溃 / 不损坏 / 不泄露)。 + // Claude Code(2.1.220+) / Seed / Relay 原生支持 /effort,Codex 亦有 reasoning + // effort;未来别的 CLI 补上后零改动自动生效,无需再逐个 adapter 加。 + // ② 全局集合刻意不带「空 topic 冷启动」能力(那只认 adapter 层的 + // defaultPassthroughCommands,见 isInitialSessionPassthrough)——/effort 是 + // 「调档」而非「开一段工作」的命令,空话题里单发 /effort 不应凭空拉起会话。 + // 对照 /goal(开启目标工作)仍留在 adapter 层,保留其冷启动语义。 + '/effort', ]); /** diff --git a/test/command-handler.test.ts b/test/command-handler.test.ts index c6a862bdf..3db33fb8f 100644 --- a/test/command-handler.test.ts +++ b/test/command-handler.test.ts @@ -454,7 +454,7 @@ vi.mock('../src/services/card-mode-store.js', () => ({ // ─── Imports (after mocks) ────────────────────────────────────────────────── -import { DAEMON_COMMANDS, SESSIONLESS_DAEMON_COMMANDS, PASSTHROUGH_COMMANDS, resolvePassthroughCommands, handleCommand, handleCardCommand, handleTermLinkCommand, parseSlashCommandInvocation, parseForceTopicInvocation } from '../src/core/command-handler.js'; +import { DAEMON_COMMANDS, SESSIONLESS_DAEMON_COMMANDS, PASSTHROUGH_COMMANDS, resolvePassthroughCommands, resolveAdapterDefaultPassthroughCommands, handleCommand, handleCardCommand, handleTermLinkCommand, parseSlashCommandInvocation, parseForceTopicInvocation } from '../src/core/command-handler.js'; import { setCardMode } from '../src/services/card-mode-store.js'; import { writeRoleFile, deleteRoleFile, writeTeamRoleFile, deleteTeamRoleFile, resolveRole, resolveRoleFile } from '../src/core/role-resolver.js'; import { setBotCapability, clearBotCapability } from '../src/services/bot-profile-store.js'; @@ -955,7 +955,7 @@ describe('/vc preparation command', () => { describe('PASSTHROUGH_COMMANDS set', () => { it('should contain expected slash commands forwarded to CLI', () => { - for (const cmd of ['/compact', '/model', '/clear', '/plugin', '/usage', '/context', '/cost', '/mcp', '/diff', '/btw']) { + for (const cmd of ['/compact', '/model', '/clear', '/plugin', '/usage', '/context', '/cost', '/mcp', '/diff', '/btw', '/effort']) { expect(PASSTHROUGH_COMMANDS.has(cmd), `Expected PASSTHROUGH_COMMANDS to contain ${cmd}`).toBe(true); } }); @@ -979,6 +979,32 @@ describe('PASSTHROUGH_COMMANDS set', () => { expect(resolvePassthroughCommands('app-2').has('/goal')).toBe(true); }); + it('exposes /effort globally to every CLI (best-effort passthrough)', () => { + // /effort 放在全局 PASSTHROUGH_COMMANDS,而非某个 adapter 的 defaultPassthroughCommands + // ——所有 CLI 都尽力透传(Claude 家族 / Codex 原生支持;其它 CLI 认不得顶多回 + // unknown-command,不崩溃)。未来新 CLI 零改动自动继承。 + expect(PASSTHROUGH_COMMANDS.has('/effort')).toBe(true); + expect(resolvePassthroughCommands('app-1').has('/effort')).toBe(true); // claude-code + expect(resolvePassthroughCommands('app-2').has('/effort')).toBe(true); // codex + // 无 bot 上下文时也回落到全局集合,仍含 /effort。 + expect(resolvePassthroughCommands(undefined).has('/effort')).toBe(true); + }); + + it('keeps /effort OUT of the adapter default layer so it never gains cold-start', () => { + // 核心语义护栏:冷启动能力(空 topic 里发命令能否拉起新会话)只认 adapter 层的 + // defaultPassthroughCommands(见 daemon.ts 的 isInitialSessionPassthrough → + // resolveAdapterDefaultPassthroughCommands),不认全局 PASSTHROUGH_COMMANDS。 + // /effort 是「调档」而非「开一段工作」的命令,必须留在全局层、绝不进 adapter 层, + // 否则空话题单发 /effort 会凭空 spawn 一个没活干的会话。这条断言锁住该语义: + // 即使有人日后误把 /effort 加回某个 adapter 的 default,resolvePassthroughCommands + // 层的可见性测试仍会全绿(全局也有),唯有这里能抓住回归。 + expect(resolveAdapterDefaultPassthroughCommands('app-1')).not.toContain('/effort'); // claude-code + expect(resolveAdapterDefaultPassthroughCommands('app-2')).not.toContain('/effort'); // codex + // /goal 相反:它是「开启目标工作」的命令,刻意留在 adapter 层保留冷启动语义。 + expect(resolveAdapterDefaultPassthroughCommands('app-1')).toContain('/goal'); + expect(resolveAdapterDefaultPassthroughCommands('app-2')).toContain('/goal'); + }); + it('does not expose Codex interactive /title through the Lark channel', () => { expect(PASSTHROUGH_COMMANDS.has('/title')).toBe(false); expect(DAEMON_COMMANDS.has('/title')).toBe(false);