这是「GSD 全景代码解析」专题的第 45 篇。
flowchart TD
A[运行时发现状态变更] --> B{变更类型}
B -->|阶段性| C[写入 .gsd/state.json]
B -->|临时性| D[写入 /tmp/gsd-*.tmp]
B -->|持久性| E[写入 ~/.gsd/global-state.json]
C --> F[状态恢复]
D --> F
E --> F
F --> G[下次启动时加载]状态文件采用分层存储策略:
- 项目级状态(
.gsd/state.json):当前项目的阶段进度、Agent 执行结果、Git 状态快照 - 全局状态(
~/.gsd/global-state.json):用户偏好、运行时选择、安装历史 - 临时状态(
/tmp/gsd-*.tmp):当前会话的缓存数据,进程结束后可清理
// state.cjs 的核心 API
const state = require('./state.cjs');
// 原子性写入:先写临时文件,再重命名,避免半写状态
state.atomicWrite('.gsd/state.json', {
phase: 'execute',
currentWave: 3,
completedTasks: ['setup', 'lint', 'test'],
lastCheckpoint: '2026-04-24T10:30:00Z'
});
// 带版本校验的读取
const current = state.read('.gsd/state.json', { schemaVersion: '2.1' });atomicWrite 是 state.cjs 中最关键的实现细节:它使用 fs.writeFileSync() 写入临时文件,然后通过 fs.renameSync() 原子替换目标文件。这保证了即使在进程崩溃的极端情况下,也不会出现损坏的 JSON 状态文件。
1.3 verify.cjs:验证引擎
verify.cjs 是 GSD 的健康检查与断言系统。在 SDK 层的 Verifier Agent(第 25 篇)负责业务逻辑的验证,而 verify.cjs 则负责运行时环境的验证——检查 Node.js 版本、确认 Git 安装、校验配置文件格式、检测端口占用等。
// verify.cjs 的验证器注册机制
const verify = require('./verify.cjs');
// 注册自定义验证器
verify.register('node-version', () => {
const semver = process.version.slice(1).split('.');
const major = parseInt(semver[0], 10);
if (major < 18) {
throw new verify.ValidationError(
`Node.js ${process.version} 不满足最低要求 (>= 18.0.0)`
);
}
});
// 执行全部验证
const results = verify.runAll();
// results: [{ name: 'node-version', status: 'pass' }, ...]verify.cjs 的验证器采用惰性执行模式:验证器在注册时不会被调用,只有当显式执行 runAll() 或 run(name) 时才会触发。这种设计允许安装器在安装过程中按需验证,而 SDK 层则在阶段转换时进行批量检查。
1.4 model-profiles.cjs:模型配置
model-profiles.cjs 维护着 GSD 支持的AI 模型配置档案。当 SDK 层需要调用 LLM API 时,它会通过引擎层查询当前模型的上下文窗口、token 限制、特性支持等元数据。
// model-profiles.cjs 的配置结构
module.exports = {
'claude-3-5-sonnet': {
contextWindow: 200000,
maxOutputTokens: 8192,
supportsVision: true,
supportsToolUse: true,
costPer1MInput: 3.00,
costPer1MOutput: 15.00,
recommendedFor: ['plan', 'execute', 'review']
},
'claude-3-haiku': {
contextWindow: 200000,
maxOutputTokens: 4096,
supportsVision: false,
supportsToolUse: true,
costPer1MInput: 0.25,
costPer1MOutput: 1.25,
recommendedFor: ['quick', 'lint', 'format']
},
'gpt-4o': {
contextWindow: 128000,
maxOutputTokens: 16384,
supportsVision: true,
supportsToolUse: true,
costPer1MInput: 5.00,
costPer1MOutput: 15.00,
recommendedFor: ['plan', 'execute']
}
};这个模块的设计理念是把模型特性从业务逻辑中抽象出来。当新模型发布时,只需更新这个配置文件,而无需修改 SDK 层的任何代码。
1.5 drift.cjs:漂移检测
drift.cjs 是 GSD 中最具创新性的模块之一。它负责检测实际执行结果与计划预期之间的漂移,并在偏离超过阈值时触发警报。
flowchart LR
A[Plan 预期输出] --> C[Drift Detector]
B[实际执行结果] --> C
C --> D{漂移量 < 阈值?}
D -->|是| E[继续执行]
D -->|否| F[记录漂移事件]
F --> G[通知 Verifier Agent]
F --> H[更新 drift-report.md]漂移检测的核心算法是将计划中的预期产出(如「应创建 3 个文件」)与实际结果(如「只创建了 1 个文件」)进行对比。drift.cjs 提供了一套声明性 API,允许工作流在关键节点注册检测点:
// drift.cjs 的使用模式
const drift = require('./drift.cjs');
// 在计划阶段注册预期
const checkpoint = drift.expect({
filesCreated: 3,
testsPassing: true,
coverageAbove: 0.8
});
// 在执行阶段检测实际结果
const actual = {
filesCreated: 1,
testsPassing: false,
coverageAbove: 0.45
};
const report = drift.compare(checkpoint, actual);
// report.driftScore = 0.67 (越高表示漂移越大)
// report.violations = ['filesCreated', 'testsPassing', 'coverageAbove']二、引擎层的职责
五个 .cjs 模块共同构成了 GSD 的底层引擎层。如果说 TypeScript SDK 是「大脑」,那么引擎层就是「小脑」——它们在大脑睡着时(例如 SDK 未安装或损坏)仍能维持基本生命体征。
2.1 运行时抽象
flowchart TB
subgraph SDK_Layer["SDK 层(TypeScript)"]
S1[Phase Runner]
S2[Context Engine]
S3[Plan Parser]
end
subgraph Engine_Layer["引擎层(CommonJS)"]
E1[core.cjs]
E2[state.cjs]
E3[verify.cjs]
E4[model-profiles.cjs]
E5[drift.cjs]
end
subgraph System_Layer["系统层"]
SYS1[Node.js 内置模块]
SYS2[文件系统]
SYS3[环境变量]
end
S1 -->|"调用 engine API"| E1
S2 -->|"读取/写入状态"| E2
S3 -->|"查询模型元数据"| E4
E1 --> E2
E1 --> E3
E1 --> E5
E2 --> SYS2
E3 --> SYS1
E4 --> SYS3引擎层为 SDK 层提供了一个稳定的抽象层。SDK 不需要知道状态是写入了 SQLite 还是 JSON,也不需要了解模型配置的来源——这些细节都被封装在引擎层中。这种分层带来的好处是:
- 可替换性:未来可以将 JSON 状态存储替换为 Redis 或 SQLite,SDK 层代码无需改动
- 可测试性:引擎层的
.cjs模块可以独立测试,不依赖 TypeScript 编译链路 - 可降级性:当 SDK 层不可用时,CLI 命令仍可通过引擎层提供基本功能
2.2 状态持久化
状态持久化是引擎层最核心的职责。GSD 的执行流程可能跨越多个进程(例如,用户先执行 gsd plan,关闭终端,第二天再执行 gsd execute),因此状态必须持久化到磁盘。
引擎层的状态管理遵循**写时复制(Copy-on-Write)**策略:
sequenceDiagram
participant SDK as SDK 层
participant Core as core.cjs
participant State as state.cjs
participant FS as 文件系统
SDK->>Core: updateState({ phase: 'execute' })
Core->>State: writeState(delta)
State->>FS: read existing state
FS-->>State: currentState
State->>State: merge(currentState, delta)
State->>FS: write to .gsd/state.json.tmp
State->>FS: rename to .gsd/state.json
FS-->>State: ok
State-->>Core: success
Core-->>SDK: ok这个流程确保了:
- 原子性:即使进程在写入过程中崩溃,也不会留下损坏的状态文件
- 隔离性:SDK 层操作的是内存中的状态副本,只有显式调用
commit()才会触发磁盘写入 - 可观测性:状态文件的每一次变更都被记录在
.gsd/state.log中,便于调试和审计
2.3 配置解析
引擎层的配置解析职责体现在 core.cjs 的配置归一化逻辑中。GSD 的配置来源非常复杂:
| 配置来源 | 优先级 | 示例 |
|---|---|---|
| CLI 参数 | 最高 | --runtime=claude-code --verbose |
| 环境变量 | 中 | GSD_RUNTIME=claude-code |
| 项目配置 | 中低 | .gsd/config.json |
| 用户全局配置 | 低 | ~/.gsd/config.json |
| 内置默认值 | 最低 | core.cjs 中的 DEFAULTS 对象 |
// core.cjs 中的配置归一化逻辑
function normalizeConfig(inputs) {
const defaults = require('./defaults.json');
const globalConfig = loadIfExists('~/.gsd/config.json');
const projectConfig = loadIfExists('.gsd/config.json');
const envConfig = parseEnvVars('GSD_');
const cliConfig = inputs;
return deepMerge(defaults, globalConfig, projectConfig, envConfig, cliConfig);
}deepMerge 的实现特别注意了数组覆盖策略:当多个来源都定义了 runtimes 数组时,高优先级的配置会完全覆盖低优先级的数组,而不是合并。这避免了「半吊子配置」的问题——用户如果在 CLI 中指定了运行时列表,就应该完全信任用户的判断。
三、bin/install.js(275KB)架构回顾
在第 4 篇中,我们首次介绍了 bin/install.js 这个 275KB 的巨型安装脚本。经过 41 篇文章的积累,现在是时候用更深入的视角重新审视它了。
3.1 为什么安装器有 275KB?
275KB 对于一个安装脚本来说似乎过大。但拆解其内容后,你会发现它并非臃肿,而是内聚——它将大量运行时适配逻辑直接嵌入脚本,避免了对网络或外部依赖的依赖。
pie title install.js 体积构成
"运行时适配模板" : 45
"内容转换引擎" : 25
"文件系统操作" : 15
"交互式 CLI UI" : 10
"验证逻辑" : 53.2 运行时检测
安装器的第一步是运行时检测——扫描用户系统中已安装的 AI 编程工具:
// install.js 中的运行时检测逻辑(简化)
const RUNTIME_DETECTION_RULES = [
{
name: 'claude-code',
check: () => fs.existsSync(path.join(os.homedir(), '.claude', 'settings.json')),
commandPath: () => which('claude')
},
{
name: 'cursor',
check: () => fs.existsSync(path.join(os.homedir(), '.cursor', 'rules')),
commandPath: () => which('cursor')
},
{
name: 'copilot',
check: () => fs.existsSync(path.join(os.homedir(), '.github', 'copilot')),
commandPath: () => null // Copilot 无独立 CLI
}
];
function detectRuntimes() {
return RUNTIME_DETECTION_RULES
.filter(rule => rule.check())
.map(rule => ({ name: rule.name, path: rule.commandPath() }));
}检测逻辑的设计体现了防御性编程:每个运行时的检测都是独立的,一个运行时的检测失败不会影响其他运行时的检测。即使 .claude/settings.json 是损坏的 JSON,detectRuntimes() 仍会返回其他已成功检测的运行时。
3.3 内容转换
安装器最核心的工程价值在于其内容转换引擎。GSD 的源文件是统一的 Markdown 格式,但不同的运行时要求不同的目标格式:
| 运行时 | 源格式 | 目标格式 | 转换操作 |
|---|---|---|---|
| Claude Code | .md | .md | 复制 + 注入 frontmatter |
| Codex | .md | .md | TOML frontmatter 转换 |
| Copilot | .md | .md | 工具名映射(Bash→execute) |
| Cursor | .md | .cursorrules | 提取规则 + 格式化 |
| Cline | .md | .clinerules | 规则提取 + 合并 |
flowchart TD
A[源 Markdown 文件] --> B{目标运行时}
B -->|Claude Code| C[注入 slash-command frontmatter]
B -->|Codex| D[转换为 TOML frontmatter]
B -->|Copilot| E[映射工具名]
B -->|Cursor| F[提取规则并格式化为 .cursorrules]
B -->|Cline| G[提取规则并合并到 .clinerules]
C --> H[写入目标路径]
D --> H
E --> H
F --> H
G --> H内容转换引擎的关键设计是纯函数转换:给定相同的输入和运行时参数,转换结果总是确定的。这使得安装过程可以被单元测试覆盖,也支持幂等安装——多次运行安装器不会产生不同的结果。
3.4 文件写入
安装器的文件写入逻辑不是简单的 fs.copyFileSync()。它实现了三阶段安全写入:
- 备份阶段:如果目标文件已存在,先将其复制到
.backup/目录 - 写入阶段:将转换后的内容写入目标路径的
.tmp文件 - 提交阶段:原子性地将
.tmp文件重命名为最终文件名
// install.js 中的安全写入逻辑
function safeWrite(targetPath, content) {
const backupDir = path.join(path.dirname(targetPath), '.backup');
const tmpPath = targetPath + '.tmp';
// 阶段 1:备份
if (fs.existsSync(targetPath)) {
fs.mkdirSync(backupDir, { recursive: true });
const backupName = `${path.basename(targetPath)}.${Date.now()}`;
fs.copyFileSync(targetPath, path.join(backupDir, backupName));
}
// 阶段 2:写入临时文件
fs.writeFileSync(tmpPath, content, 'utf-8');
// 阶段 3:原子提交
fs.renameSync(tmpPath, targetPath);
}这种设计确保了安装过程是可逆的:如果安装后发现问题,用户可以从 .backup/ 目录恢复原始文件。同时,原子提交避免了「半写文件」的问题——用户在安装过程中按下 Ctrl+C 不会留下损坏的配置文件。
四、引擎层与 SDK 的关系
经过以上分析,我们可以清晰地看到引擎层与 SDK 之间的协作关系:
flowchart TB
subgraph User["用户"]
U1[CLI 命令]
U2[工作流文件]
end
subgraph SDK["SDK 层(TypeScript)"]
S1[Phase Runner]
S2[Context Engine]
S3[Plan Parser]
S4[Prompt Builder]
end
subgraph Engine["引擎层(CommonJS)"]
E1[core.cjs
调度中心]
E2[state.cjs
状态持久化]
E3[verify.cjs
环境验证]
E4[model-profiles.cjs
模型配置]
E5[drift.cjs
漂移检测]
end
subgraph Installer["安装器"]
I1[install.js
运行时检测]
I2[install.js
内容转换]
I3[install.js
文件写入]
end
subgraph System["系统层"]
SYS1[文件系统]
SYS2[AI 运行时]
end
U1 --> S1
U2 --> S2
S1 -->|"调用 engine.run()"| E1
S2 -->|"读写状态"| E2
S3 -->|"查询模型限制"| E4
S4 -->|"验证环境"| E3
S1 -->|"检测漂移"| E5
E1 --> I1
I1 --> I2
I2 --> I3
I3 --> SYS1
E2 --> SYS1
I1 --> SYS2这张图揭示了 GSD 架构的一个关键设计决策:引擎层与 SDK 层不是上下级关系,而是并列协作关系。
- SDK 层负责「高级编排」:阶段管理、上下文组装、Agent 调度——这些工作需要类型安全和复杂的抽象
- 引擎层负责「底层基础设施」:状态持久化、环境验证、配置解析——这些工作需要稳定性和最小依赖
- 安装器负责「部署适配」:运行时检测、内容转换、文件写入——这些工作需要独立运行、不依赖项目环境
三者通过清晰的接口契约协作:
// SDK 层调用引擎层的契约示例
interface EngineAPI {
init(config: RuntimeConfig): Promise<void>;
getState(): ProjectState;
setState(partial: Partial<ProjectState>): void;
verify(checks: string[]): VerificationResult[];
getModelProfile(modelId: string): ModelProfile;
detectDrift(expected: Expectation, actual: Actual): DriftReport;
}这种分层架构的优势在于:
- 渐进式采用:用户可以先通过安装器获得基础功能,再逐步引入 SDK 层的高级特性
- 故障隔离:SDK 层的类型错误不会波及引擎层,引擎层的文件系统错误不会导致 SDK 层崩溃
- 独立演进:引擎层可以针对稳定性优化(减少依赖、增加容错),SDK 层可以针对功能性优化(新特性、更好的类型推断)
五、小结
本文系统解析了 GSD 底层引擎层与安装器的架构设计:
bin/lib/目录:五个.cjs模块构成了 GSD 的底层运行时,总代码量约 23KB,却承载了状态持久化、环境验证、模型配置、漂移检测四大基础设施职责core.cjs:最小依赖原则的典范,仅依赖 Node.js 内置模块,提供生命周期管理和配置归一化state.cjs:原子性写入 + 分层存储,确保进程崩溃后状态可恢复verify.cjs:惰性执行的验证器注册机制,支持安装器和 SDK 层的按需验证model-profiles.cjs:模型特性抽象层,新模型发布时只需更新配置drift.cjs:声明式漂移检测 API,连接计划预期与实际执行结果install.js(275KB):内置完整运行时检测、内容转换和文件写入引擎,支持 15+ 运行时的幂等安装
引擎层与 SDK 层的协作关系充分体现了 GSD 的工程成熟度:不是把所有逻辑塞进一个层,而是让每一层专注于自己最擅长的事情。
预告:第 46 篇——SDK 扩展开发指南
在下一篇文章中,我们将从「阅读源码」转向「动手扩展」,深入讲解如何基于 GSD SDK 开发自定义扩展:
- Runtime 适配器开发:如何为新的 AI 编程工具添加运行时支持?
- Phase Hook 开发:如何在阶段执行前后注入自定义逻辑?
- Query Handler 开发:如何扩展
GSD.SDK.query()的查询能力? - 插件打包与发布:如何将扩展打包为可复用的 GSD 插件?
下一篇预告: 第 46 篇《SDK 扩展开发指南》
我们将深入解析 GSD SDK 的扩展机制,包括如何注册自定义 Agent、如何扩展命令系统、以及如何开发第三方插件。这是动手改造 GSD 的实战指南,敬请期待。