Claude Code 的三个斜杠命令采用了不同的运行时机制。/compact 改写会话上下文,/simplify 通过 bundled Skill 注入代码审查流程,/goal 用状态、工具调用和自动续跑管理长任务。
核心 CLI 没有完整开源。Anthropic 官方仓库提供 CHANGELOG、插件和扩展类型;具体调用链引用 Claude Code Best 的 TypeScript 复刻实现,并参考 Open-ClaudeCode 的 npm source map 归档。复刻代码只用于说明机制。
2.1.63 CHANGELOG 将 /simplify 和 /batch 列为 bundled slash command;claude-code.d.ts 将 $.session.compact() 定义为与 /compact 相同的会话压缩入口。
一、三个命令的注册类型
复刻实现用 Command 联合类型区分命令的加载和执行协议:
export type LocalCommandResult = | { type: 'text'; value: string } | { type: 'compact'; compactionResult: CompactionResult } | { type: 'skip' }
type LocalCommand = { type: 'local' supportsNonInteractive: boolean load: () => Promise<LocalCommandModule>}
type LocalJSXCommand = { type: 'local-jsx' load: () => Promise<LocalJSXCommandModule>}
export type PromptCommand = { type: 'prompt' source: SettingSource | 'builtin' | 'plugin' | 'bundled' getPromptForCommand( args: string, context: ToolUseContext, ): Promise<ContentBlockParam[]>}这段类型定义位于复刻实现的 src/types/command.ts。
三个命令对应的注册类型如下:
| 命令 | 注册类型 | 主要副作用 | 复刻实现的源码入口 |
|---|---|---|---|
/compact | local | 用摘要和边界消息替换旧上下文 | src/commands/compact/index.ts |
/simplify | prompt,来源为 bundled | 给当前模型注入一套审查和修复流程 | src/skills/bundled/simplify.ts |
/goal | local-jsx | 创建或改变持久目标,并触发后续自动查询 | src/commands/goal/index.ts |
它们经过同一个命令发现器,执行路径如下:
在复刻实现的 src/commands.ts 中,compact 直接加入 COMMANDS 数组,goal 只在 GOAL feature flag 打开时加入。bundled Skill 通过 registerBundledSkill() 注册,再由 getSkills() 与文件型 Skill、插件 Skill 合并。因此,/simplify 虽然出现在 slash command 菜单中,运行时仍按 Skill 处理。
二、/compact:一次受控的上下文重写
2.1 注册阶段只描述能力,不执行压缩
注册对象只描述命令元数据和懒加载入口:
const compact = { type: 'local', name: 'compact', description: 'Clear conversation history but keep a summary in context. Optional: /compact [instructions for summarization]', isEnabled: () => !isEnvTruthy(process.env.DISABLE_COMPACT), supportsNonInteractive: true, argumentHint: '<optional custom summarization instructions>', load: () => import('./compact.js'),} satisfies Command这段注册代码体现了两点:
load()使用动态 import,启动时只把命令元数据放进菜单,不提前加载摘要、Hook、token 计算和 API 调用相关模块。DISABLE_COMPACT只影响命令是否可用。自动压缩是否开启还要经过另一套配置和 feature flag 判断,因此“隐藏/compact”与“关闭自动压缩”不是同一个开关。
2.2 手动命令的主调用链
src/commands/compact/compact.ts 的 call() 可以压缩成下面的伪代码:
/compact args -> getMessagesAfterCompactBoundary(messages) -> 没有自定义指令时,先尝试 session-memory compaction -> reactive-only 模式走 reactiveCompactOnPromptTooLong -> microcompactMessages(messages) -> compactConversation(messagesForCompact, customInstructions) -> 返回 { type: "compact", compactionResult }call() 先调用 getMessagesAfterCompactBoundary(),只处理最近一个压缩边界之后的消息。消息为空时,命令直接报错。
2.3 Micro Compact 先处理工具结果
microcompactMessages() 先处理工具结果。在启用 cached microcompact 且模型受支持时,源码扫描可压缩的 tool_result,按消息分组记录它们,再通过 cache edit 删除旧结果。消息本身可以保持不变,删除操作延迟到 API 层执行。
完整压缩前,代码会先尝试 microcompact:
cached microcompact 只允许主线程使用,子 Agent 不会把自己的工具 ID 写入主线程的全局缓存状态。
2.4 compactConversation() 做了什么
摘要过程位于 src/services/compact/compact.ts 的 compactConversation()。
记录 token 并运行 PreCompact hooks。
函数先用 tokenCountWithEstimation(messages) 估算 preCompactTokenCount,再触发 executePreCompactHooks()。Hook 可以返回新的摘要指令,最终通过 mergeHookInstructions() 与用户在 /compact ... 后面写的指令合并,用户指令排在前面。
构造摘要请求。
getCompactPrompt() 要求模型只输出文本,不调用工具,并使用 <analysis> 和 <summary> 标签。摘要请求复用主会话的工具 Schema 时,工具调用会使这次压缩请求失去摘要文本。
摘要提示词要求保留用户目标、文件名、函数签名、代码修改、错误修复和安全约束。formatCompactSummary() 随后移除 <analysis> 草稿,只把 <summary> 写回会话。
优先复用 prompt cache。
streamCompactSummary() 首选 runForkedAgent(),复用主会话的系统提示词、工具和缓存前缀。失败或开关关闭时,代码调用 queryModelWithStreaming(),并执行以下处理:
- 把图片和文档替换成
[image]、[document]标记,避免摘要请求本身因为媒体超限; - 移除压缩后会重新注入的 Skill attachment;
- 禁用 thinking,只保留文本摘要;
- 只允许读取类工具,且拒绝工具调用。
处理摘要请求超长。
历史过长时,摘要请求本身也可能超出窗口。代码会重试,并按 API round 丢弃最早的一组消息;仍然失败时返回 Conversation too long to summarize,提示用户使用 /clear。
保存需要恢复的上下文。
摘要生成后,代码保存 readFileState,清理读取缓存,并生成几类附件:最近读取的少量文件、仍在运行的 Agent 信息、计划文件、plan mode 指令、已经调用过的 Skill,以及工具、Agent、MCP 指令的 delta。复刻实现限制了单文件、单 Skill 和总附件的 token 数,也限制了恢复文件数量。
原始 transcript 仍然存在。当前 API 看到的是摘要、保留的近期消息和这些附件。
写入边界、摘要消息和 Hook 结果。
createCompactBoundaryMessage() 生成一个 system / compact_boundary 消息,记录触发方式、压缩前 token 数以及最后一条旧消息的 UUID。之后按固定顺序组装新消息:
boundaryMarkersummaryMessagesmessagesToKeepattachmentshookResultsQueryEngine 和 SDK 层识别这个边界。官方 SDK 类型说明,$.session.compact() 与 /compact 触发同一事件,压缩后的 transcript 由摘要和保留消息组成,消息流中可以观察到 compact_boundary。
2.5 手动压缩、自动压缩和 Reactive Compact
手动、自动和 Reactive Compact 共用 compactConversation(),触发路径不同:
| 触发方式 | 入口 | 特点 |
|---|---|---|
手动 /compact | commands/compact/compact.ts | 支持用户附加摘要指令,成功后立即返回本地命令结果 |
| 自动压缩 | services/compact/autoCompact.ts | 由 token 阈值和配置触发,带递归保护和失败熔断 |
| Reactive Compact | reactiveCompactOnPromptTooLong() | 等 API 返回 prompt-too-long 后再压缩,由实验开关控制 |
官方类型定义把 session.compact 的 trigger 暴露为 plugin,并允许 Hook veto。cached microcompact、reactive 模式和具体阈值属于版本实现细节。
三、/simplify:并行审查流程
3.1 官方确认它是 bundled slash command
Anthropic 官方 CHANGELOG 的 2.1.63 条目写明“Added /simplify and /batch bundled slash commands”。核心 CLI 的完整实现没有在官方仓库公开。
在 Claude Code Best 中,/simplify 的实现位于 src/skills/bundled/simplify.ts:
export function registerSimplifySkill(): void { registerBundledSkill({ name: 'simplify', description: 'Review changed code for reuse, quality, and efficiency, then fix any issues found.', userInvocable: true, async getPromptForCommand(args) { let prompt = SIMPLIFY_PROMPT if (args) prompt += `\n\n## Additional Focus\n\n${args}` return [{ type: 'text', text: prompt }] }, })}registerBundledSkill() 将它转换成 type: 'prompt' 命令,并标记 source: 'bundled'、loadedFrom: 'bundled'。命令发现器再把它与项目 .claude/skills 和插件 Skill 合并。
3.2 Prompt 的三个阶段
SIMPLIFY_PROMPT 为主 Agent 规定了三个阶段。
确定变更集合。
模型先运行 git diff,有暂存区时使用 git diff HEAD;工作树没有变化时,改为审查用户提到或本轮编辑过的文件。
并行启动三个 Agent。
提示词要求在一条消息中并发调用三次 Agent 工具,并把完整 diff 传给每个 Agent:
| Agent | 检查重点 |
|---|---|
| Code Reuse Review | 搜索已有 helper,识别重复函数、手写路径处理和重复类型守卫 |
| Code Quality Review | 检查冗余状态、参数膨胀、复制粘贴、泄漏抽象、字符串类型和无意义 JSX 嵌套 |
| Efficiency Review | 检查重复计算、串行执行、热路径膨胀、内存泄漏和过宽读取 |
三个 Agent tool call 在同一条模型响应中发出,由工具调度层并发执行。/simplify 本身不包含 AST 分析器或独立的 lint 引擎。
汇总并修复。
主 Agent 等待三个结果,合并发现并修改代码。误报可以跳过,但需要在最终摘要中说明。/simplify 后面的参数会追加到 ## Additional Focus,可用于限定性能、API 兼容性或目录范围。
3.3 执行边界
/simplify 通过提示词编排审查和修复:
它使用正常的权限、工作树和工具调用协议,也不保证采纳每条建议。三个 Agent 的结果取决于模型对 diff、仓库搜索和误报的判断。
四、/goal:跨轮次状态机
4.1 命令入口
复刻实现的注册对象如下:
const goal = { type: 'local-jsx', name: 'goal', description: 'Set or view a persistent goal that drives auto-continuation across turns', argumentHint: '[<objective> | status | clear | pause | resume | complete]', bridgeSafe: false, load: () => import('./goal.js'),} satisfies Commandgoal.tsx 处理用户输入和状态操作,支持:
| 输入 | 状态操作 |
|---|---|
/goal <objective> | 创建目标;已有未完成目标时先弹出替换确认框 |
/goal 或 /goal status | 显示目标、状态、耗时、token 和续跑轮数 |
/goal pause / resume | 暂停或恢复自动续跑 |
/goal continue | 达到最大轮数后,清零续跑计数并由用户明确继续 |
/goal complete | 手动完成目标 |
/goal clear | 清理目标并写入 tombstone,避免恢复会话时复活旧目标 |
命令完成回调可以设置 shouldQuery: true,也可以插入隐藏的 metaMessages。设置新目标时,源码会插入 <goal-objective-updated> 元消息,UI 则显示截断后的摘要。
4.2 GoalState
状态机位于复刻实现的 src/services/goal/goalState.ts,并按 session ID 存在 Map<string, GoalState> 中。关键字段包括:
type GoalState = { objective: string status: 'active' | 'paused' | 'blocked' | 'budget_limited' | 'usage_limited' | 'max_turns' | 'complete' tokenBudget: number | null tokensUsed: number startTime: number pausedAt: number | null accumulatedActiveMs: number blockedAttempts: number lastBlockReason: string | null turnsExecuted: number}状态转移规则如下:
pause把当前 active 时间累加到accumulatedActiveMs,恢复时重新设置startTime,所以展示的是有效工作时间,不是墙上时钟时间。- token 计数在 cost tracker 收到每次 API usage 后更新,累计达到
tokenBudget就转成budget_limited。 - 连续三次相同原因的
blocked报告才会转成blocked;原因变化会重置连续计数。 - 自动续跑最多
MAX_GOAL_TURNS = 150轮,达到上限后变成max_turns,必须由用户输入/goal continue才能清零重启。
goalStorage.ts 每次变更都会向当前会话的 JSONL transcript 写入 goal checkpoint。JSONL 是每行一个 JSON 记录;/goal clear 追加 goal-cleared tombstone(删除标记)。恢复会话时,读取器按 session ID 取最后一个状态,/resume 便可恢复目标。
4.3 模型通过 GoalTool 报告完成或阻塞
模型通过 GoalTool.ts 报告完成或阻塞。工具输入限制为:
z.strictObject({ action: z.enum(['get', 'update']).optional(), status: z.enum(['complete', 'blocked']).optional(), reason: z.string().optional(),})get 读取当前快照;update 只能将目标标记为 complete 或 blocked。完成时工具生成 token、有效耗时和续跑轮数报告,再调用 completeGoal();阻塞时调用 recordBlockedAttempt(),相同条件连续三轮后才会进入 blocked。
目标状态由应用代码保存,模型只能通过 schema 约束的工具修改它。
4.4 Idle hook 触发下一轮
自动续跑由 useGoalContinuation() 驱动。Hook 在 isLoading 从 true 变成 false 时检查是否需要下一轮:
一轮查询结束 -> 检查用户队列、活动 JSX、plan mode 和 abort 状态 -> 读取 GoalState -> budget_limited: 注入一次收尾提示 -> active 且未到 MAX_GOAL_TURNS: turnsExecuted += 1 -> enqueue(<goal-steering type="continuation">...</goal-steering>) -> 下一轮正常 query如果队列里有 /goal pause 或普通用户消息,Hook 会跳过本次自动续跑,等用户消息处理完再判断。
buildContinuationPrompt() 生成带审计规则的 meta message:
- 保持原始目标范围,不因为一轮没做完就擅自缩小目标。
- 完成前执行 Completion Audit,逐项寻找测试、文件内容或命令结果等证据。
- 遇到障碍先继续尝试,只有同一阻塞原因连续三轮才允许标记 blocked。
- 真正完成或确认阻塞时,通过
GoalTool更新状态。
系统上下文还会包含一个短 XML 目标摘要:
<active-goal status="active" elapsed="4m 12s" tokens="18400" budget="50000" turns="3">为 monorepo 添加可回滚的数据库迁移并补齐测试</active-goal>这个摘要把目标状态带入每轮上下文,完整的审计规则只在续跑提示中出现。
4.5 token 预算
src/cost-tracker.ts 根据每次 API 响应的 input、output、cache read 和 cache creation token 调用 updateGoalTokens()。达到预算后,idle hook 注入一次 budget_limit 提示,要求模型停止新的工具调用并汇报已完成和剩余工作。
这条链路可以画成:
预算控制由应用层执行。用户仍然可以修改或清除目标,模型也可能在收到预算提示前发起最后一个工具调用。
五、把三个命令放在一起比较
| 维度 | /compact | /simplify | /goal |
|---|---|---|---|
| 改变的对象 | 当前会话消息表示 | 当前 Agent 的审查指令 | 跨轮次目标状态和消息队列 |
| 核心机制 | 摘要 API + compact boundary + 恢复附件 | bundled prompt + 三个并行 Agent | GoalState + GoalTool + idle hook |
| 是否直接修改历史 | 是,旧消息被摘要替代 | 否,正常工具调用产生修改 | 不直接改历史,但会追加 meta message |
| 是否持久化 | transcript 记录 compact boundary | 通过普通文件修改持久化 | JSONL 中每次写入 goal checkpoint |
| 失败/停止策略 | prompt-too-long 重试,最终建议 /clear | 主 Agent 判断误报并跳过 | budget、blocked、max_turns、pause |
| 实现边界 | 摘要是有损的 | 结果由模型和工具调用决定 | 状态机有硬上限 |
三者的控制对象不同:
/compact改变“模型还能看到什么”。/simplify改变“模型如何审查当前改动”。/goal改变“模型何时继续下一轮,以及何时必须停下”。
六、版本边界
引用版本:Claude Code Best commit 为 77a7934e15d69da13879112ed7db695c9ee7a52a,Open-ClaudeCode commit 为 2980105e5ae5910d50c2e8adfa0ce6b5e4907bcc。核心 CLI 的实现会随版本变化,实验开关、阈值和内置提示词都可能调整。稳定的部分是命令语义和公开扩展接口;函数名、token 常量和 prompt 文本都不应视为长期契约。
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时








