Phase Runner 核心引擎

📑 目录

这是「GSD 全景代码解析」专题的第 39 篇。在本系列中,我们将逐层拆解 Get Shit Done (GSD) 这一 56K+ stars 的 Meta-Prompting 框架,从命令系统到工作流编排,从 Agent 设计到上下文工程,再到 SDK 运行时内核,带你一览上下文驱动开发的工程全貌。


在前 38 篇文章中,我们系统梳理了 GSD 的命令系统、工作流编排层、Agent 体系、上下文工程和参考文档体系。这些内容构成了 GSD 的「静态资产」——它们定义了规则、流程和规范,但真正让这些静态资产动起来的,是位于 SDK 层的一个核心模块:phase-runner.ts

如果说工作流是「剧本」,Agent 是「演员」,那么 Phase Runner 就是「舞台导演」——它负责调度演员、控制灯光、管理幕布开关,并在演出出错时决定是否暂停或继续。作为 GSD SDK 中体积最大的单个模块(39KB),phase-runner.ts 承载着阶段执行、状态机管理和生命周期控制三大核心职责。

本文将深入拆解这个「舞台导演」的内部机制。


一、phase-runner.ts 概览:39KB 的引擎巨兽

phase-runner.ts 是 GSD SDK 中体积最大、逻辑最复杂的模块。在典型的 GSD 项目中,SDK 层的总代码量约为 80-120KB,而 Phase Runner 独占 39KB,占比超过 30%。这个比例并非偶然——它反映了 Phase Runner 在整个系统中的枢纽地位。

1.1 模块定位:编排与执行的桥梁

在 GSD 的架构分层中,Phase Runner 位于一个承上启下的关键位置:

flowchart TD
    subgraph 上层抽象层
        CLI["CLI 命令入口
gsd-cli.ts"] Config["配置系统
config-loader.ts"] end subgraph 核心引擎层 PR["Phase Runner
phase-runner.ts (39KB)"] end subgraph 下层基础设施 WF["工作流解析器
workflow-parser.ts"] AG["Agent 调度器
agent-dispatcher.ts"] GH["Git 操作层
git-operations.ts"] FS["文件系统层
fs-adapter.ts"] end CLI -->|"解析命令参数"| PR Config -->|"加载配置上下文"| PR PR -->|"解析工作流模板"| WF PR -->|"派发 Agent 任务"| AG PR -->|"提交状态变更"| GH PR -->|"读写阶段文件"| FS

Phase Runner 的上游是 CLI 和配置系统,下游是工作流解析器、Agent 调度器、Git 操作层和文件系统层。它的核心职责可以概括为一句话:

将工作流定义(workflow)转化为可执行的阶段序列,并在每个阶段中正确调度 Agent、管理状态、处理异常。

1.2 核心职责拆解

39KB 的代码量分布在以下五个核心职责域中:

职责域代码量(估算)核心功能
阶段执行流程~12KB阶段初始化、上下文组装、Agent 调用、结果处理、状态更新
状态机设计~10KB状态定义、状态转换、事件驱动、持久化
生命周期管理~8KB阶段钩子、错误处理、超时控制、重试机制
工作流衔接~5KB解析 workflows/*.md、变量替换、条件分支
工具函数~4KB日志、工具函数、类型守卫

这种代码分布反映了设计优先级:执行流程和状态机是核心,生命周期管理是保障,工作流衔接是边界

1.3 为什么需要 Phase Runner

一个自然的问题是:为什么工作流不直接调用 Agent,而需要一个中间层?答案在于 GSD 的设计哲学——Separation of Orchestration and Execution(编排与执行分离)

工作流文件(workflows/*.md)是声明式的,它们描述「做什么」,但不描述「怎么做」。Phase Runner 是命令式的,它负责:

  • 何时进入下一个阶段(基于状态机判断)
  • 如何组装 Agent 的上下文(基于阶段配置和运行时状态)
  • 怎样处理 Agent 的返回结果(成功、失败、部分成功、需要人工介入)
  • 是否需要重试或回滚(基于错误策略和超时配置)

没有 Phase Runner,工作流文件将不得不混入大量命令式逻辑,破坏其声明式的纯净性。


二、阶段执行流程:从计划到落地的五步法

Phase Runner 的核心工作循环是阶段执行流程。每个 GSD 项目的生命周期都由一系列 phase(阶段)组成,例如 plan-phaseexecute-phaseverify-phase 等。Phase Runner 负责依次执行这些阶段,并确保每个阶段的输出正确传递给下一个阶段。

阶段执行流程可以抽象为五个步骤:

flowchart LR
    subgraph "阶段执行五步法"
        direction LR
        S1["1. 阶段初始化
Phase Initialization"] --> S2["2. 上下文组装
Context Assembly"] S2 --> S3["3. Agent 调用
Agent Invocation"] S3 --> S4["4. 结果处理
Result Processing"] S4 --> S5["5. 状态更新
State Update"] end S5 -.->|"下一阶段"| S1

下面逐一解析每个步骤的实现细节。

2.1 阶段初始化:从 STATE.md 到运行时上下文

阶段初始化是 Phase Runner 执行一个 phase 的第一步。它的任务是将静态的阶段定义转化为可执行的运行时上下文。

初始化流程:

flowchart TD
    Start(["开始阶段初始化"]) --> LoadState["加载 STATE.md"]
    LoadState --> ParsePhase["解析阶段定义
从 workflows/*.md"] ParsePhase --> CheckDeps["检查依赖阶段"] CheckDeps -->|"依赖未完成"| Wait["等待/报错"] CheckDeps -->|"依赖已完成"| LoadCtx["加载阶段上下文
phase context"] LoadCtx --> Validate["验证输入条件
preconditions"] Validate -->|"验证失败"| Abort["中止执行"] Validate -->|"验证通过"| InitDone(["初始化完成"]) Wait --> Abort

关键代码逻辑(概念伪代码):

async function initializePhase(
  phaseId: string,
  projectContext: ProjectContext
): Promise<PhaseRuntimeContext> {
  // 1. 从 STATE.md 读取当前项目状态
  const state = await loadState(projectContext.statePath);

  // 2. 解析阶段定义(从 workflows/*.md 中读取该阶段的配置)
  const phaseDef = await workflowParser.parsePhase(phaseId);

  // 3. 检查依赖阶段是否已完成
  for (const dep of phaseDef.dependsOn) {
    if (!state.completedPhases.includes(dep)) {
      throw new PhaseDependencyError(
        `Phase ${phaseId} depends on ${dep}, which is not completed.`
      );
    }
  }

  // 4. 加载阶段上下文(模板变量、参考文档路径等)
  const phaseContext = await loadPhaseContext(phaseDef, projectContext);

  // 5. 验证前置条件
  if (phaseDef.preconditions) {
    const ok = await validatePreconditions(phaseDef.preconditions, phaseContext);
    if (!ok) {
      throw new PreconditionError(`Preconditions not met for phase ${phaseId}`);
    }
  }

  // 6. 组装运行时上下文
  return {
    phaseId,
    phaseDef,
    state,
    context: phaseContext,
    startTime: Date.now(),
    status: PhaseStatus.INITIALIZING,
  };
}

设计要点:

  • STATE.md 是真相源:所有阶段的状态信息(已完成、进行中、失败)都来源于 STATE.md,而不是内存中的临时状态。这保证了崩溃恢复的可行性。
  • 依赖检查是硬约束:如果一个阶段的依赖未完成,Phase Runner 会立即抛出 PhaseDependencyError,而不是静默等待。这种「fail-fast」策略避免了死锁和状态漂移。
  • 前置条件(preconditions)是软约束:与依赖不同,preconditions 是阶段定义中可选的条件表达式(如"Git 工作树必须干净"),用于在阶段执行前进行环境检查。

2.2 上下文组装:将静态模板转化为动态提示

上下文组装是 Phase Runner 最具技术含量的环节之一。它的任务是将工作流文件中的静态模板转化为适合特定 Agent 的动态提示(prompt)。

组装流程:

flowchart TD
    Start(["开始上下文组装"]) --> LoadTemplate["加载阶段模板
workflows/phase.md"] LoadTemplate --> ResolveVars["解析模板变量
"] ResolveVars --> LoadRefs["加载参考文档
references/*.md"] LoadRefs --> InjectState["注入当前状态
STATE.md 片段"] InjectState --> BuildPrompt["构建 Agent Prompt"] BuildPrompt --> SetParams["设置模型参数
temperature, max_tokens"] SetParams --> Done(["上下文组装完成"])

变量解析机制:

GSD 工作流模板使用 Mustache 风格的变量插值:{{variable_name}}。Phase Runner 在组装上下文时,会按以下优先级解析这些变量:

优先级变量来源示例说明
1阶段参数{{phase_name}}从阶段定义中直接获取
2项目配置{{project_name}}config.json 中读取
3运行时状态{{current_phase}}STATE.md 中动态提取
4环境变量{{env.API_KEY}}从进程环境变量中读取
5系统内置{{timestamp}}Phase Runner 自动注入

参考文档加载策略:

GSD 的一个核心设计是「参考文档(References)驱动」。Phase Runner 不会将所有参考文档一次性注入 Agent 的上下文,而是根据阶段定义中的 references 字段进行选择性加载

async function loadReferences(
  refConfig: ReferenceConfig,
  projectContext: ProjectContext
): Promise<string[]> {
  const loaded: string[] = [];

  for (const ref of refConfig.includes) {
    const content = await fs.readFile(
      path.join(projectContext.refsDir, ref),
      'utf-8'
    );

    // 根据阶段需求截取相关章节
    if (refConfig.sections) {
      loaded.push(extractSections(content, refConfig.sections));
    } else {
      loaded.push(content);
    }
  }

  // 应用上下文预算控制
  return applyContextBudget(loaded, refConfig.maxTokens);
}

**上下文预算(Context Budget)**是 Phase Runner 的一个关键机制。它确保注入 Agent 的参考文档总量不超过模型的上下文窗口限制。如果参考文档总长度超出预算,Phase Runner 会按优先级截断或摘要处理。

2.3 Agent 调用:从 Prompt 到执行的桥梁

上下文组装完成后,Phase Runner 进入第三步:调用 Agent。这一步的本质是将组装好的 prompt 提交给 Agent 调度器(Agent Dispatcher),并等待执行结果。

调用流程:

sequenceDiagram
    participant PR as Phase Runner
    participant AD as Agent Dispatcher
    participant LLM as LLM API
    participant WS as 工作区

    PR->>PR: 组装 Agent Prompt
    PR->>AD: dispatch(agentId, prompt, config)
    AD->>AD: 选择模型 & 工具
    AD->>LLM: 发送请求
    activate LLM
    LLM-->>AD: 流式响应 (SSE)
    AD-->>PR: 实时进度回调
    LLM->>WS: 工具调用(文件读写等)
    WS-->>LLM: 工具结果
    LLM-->>AD: 最终响应
    deactivate LLM
    AD-->>PR: 返回 AgentResult

关键设计决策:

  1. 流式响应(Streaming):Phase Runner 通过 SSE(Server-Sent Events)接收 Agent 的流式输出,并在控制台实时渲染进度。这不仅提升了用户体验,还允许 Phase Runner 在 Agent 执行过程中进行中断检查(例如用户按下 Ctrl+C)。

  2. 工具调用代理:Agent 在执行过程中可能会调用工具(如读写文件、执行 shell 命令)。这些工具调用不是由 Agent 直接执行的,而是通过 Agent Dispatcher 代理到工作区。Phase Runner 会记录所有工具调用的日志,用于后续的审计和回溯。

  3. 并发控制:如果一个阶段需要同时调度多个 Agent(例如 execute-phase 的 Wave 执行模型),Phase Runner 会维护一个并发池,确保同时运行的 Agent 数量不超过配置上限(默认为 3)。

Agent 调用接口:

interface AgentInvocationConfig {
  agentId: string;           // 如 "gsd-executor"
  model?: string;            // 覆盖默认模型
  temperature?: number;      // 采样温度
  maxTokens?: number;        // 最大输出长度
  timeout?: number;          // 超时时间(毫秒)
  tools?: string[];          // 允许使用的工具列表
  streaming?: boolean;       // 是否启用流式输出
}

interface AgentResult {
  status: 'success' | 'failure' | 'partial' | 'interrupted';
  output: string;            // Agent 的文本输出
  artifacts?: Artifact[];    // 生成的文件、代码块等
  toolCalls?: ToolCallLog[]; // 工具调用记录
  metrics?: UsageMetrics;    // Token 使用量等
}

2.4 结果处理:从原始输出到结构化产物

Agent 返回的结果通常是原始文本(可能包含 Markdown、代码块、文件路径等)。Phase Runner 的第四步是结果处理——将原始输出解析为结构化产物,并验证其质量。

处理流程:

flowchart TD
    Start(["Agent 返回结果"]) --> Parse["解析输出结构"]
    Parse --> Extract["提取产物
代码块/文件路径/SUMMARY.md"] Extract --> Validate["验证产物完整性"] Validate -->|"验证失败"| Retry["触发重试/回滚"] Validate -->|"验证通过"| Store["存储产物到工作区"] Store --> Diff["生成变更 Diff"] Diff --> Done(["结果处理完成"])

产物提取策略:

不同的 Agent 类型会产出不同格式的结果。Phase Runner 维护了一套产物解析器注册表

Agent 类型产物格式解析器
gsd-plannerPLAN.mdMarkdownPlanParser
gsd-executor代码块 + SUMMARY.mdCodeArtifactParser
gsd-verifier验证报告ReportParser
gsd-researcher研究笔记NoteParser

gsd-executor 为例,其输出通常包含:

  1. 代码块(fenced code blocks):实际修改的代码
  2. 文件路径标注(如 // file: src/utils.ts):代码块对应的文件
  3. SUMMARY.md 片段:执行摘要,用于更新 STATE.md

Phase Runner 的 CodeArtifactParser 会提取这些信息,并生成一个 Artifact[] 数组:

interface Artifact {
  type: 'code' | 'markdown' | 'config' | 'diff';
  targetPath: string;        // 目标文件路径
  content: string;           // 内容
  language?: string;         // 代码语言
  description?: string;      // 变更描述
}

验证机制:

产物提取后,Phase Runner 会进行一系列验证:

  • 路径合法性:目标路径是否在项目目录内(防止路径遍历攻击)
  • 语法检查(可选):如果是代码文件,是否可通过编译/解析
  • 冲突检测:目标文件是否已被其他并发 Agent 修改
  • 格式合规:如果是 PLAN.mdSTATE.md,是否符合 GSD 规范

2.5 状态更新:持久化阶段成果

阶段执行的最后一步是将成果持久化到项目状态文件中。这是保证 GSD 项目可恢复性的关键。

更新流程:

flowchart TD
    Start(["状态更新"]) --> WriteArtifacts["写入产物到工作区"]
    WriteArtifacts --> UpdateState["更新 STATE.md"]
    UpdateState --> CheckCheckpoints["检查是否需要检查点"]
    CheckCheckpoints -->|"需要人工介入"| CreateCheckpoint["创建检查点
checkpoint.md"] CheckCheckpoints -->|"自动通过"| GitCommit["Git 原子提交"] CreateCheckpoint --> Pause(["暂停等待用户"]) GitCommit --> LogAudit["写入审计日志"] LogAudit --> Done(["阶段完成"])

STATE.md 更新策略:

Phase Runner 使用原子更新策略修改 STATE.md

  1. 读取当前 STATE.md 到内存
  2. 根据阶段执行结果更新对应字段(如 current_phasecompleted_phasespending_tasks
  3. 将更新后的内容写入临时文件 .STATE.md.tmp
  4. 重命名临时文件覆盖原文件(利用文件系统的原子性)
  5. 执行 git add STATE.mdgit commit(如果配置了自动提交)

这种策略保证了即使在写入过程中进程崩溃,STATE.md 也不会处于损坏的中间状态。

检查点(Checkpoint)机制:

某些阶段在执行过程中可能需要人工介入(例如审查生成的计划、确认破坏性变更)。Phase Runner 支持在阶段定义中声明检查点:

# 阶段定义片段(来自 workflows/*.md)
checkpoints:
  - name: "review_plan"
    condition: "phase == 'plan-phase'"
    prompt: "请审查生成的 PLAN.md,确认后输入 'yes' 继续"

当检查点条件满足时,Phase Runner 会暂停阶段执行,生成 checkpoint.md 文件,并在控制台等待用户输入。只有在用户确认后,阶段才会继续。


三、状态机设计:阶段生命周期的精确控制

如果说阶段执行流程是 Phase Runner 的「骨架」,那么状态机就是它的「神经系统」。状态机负责精确控制每个阶段的生老病死,确保阶段在任何时刻都处于一个明确定义的状态。

3.1 状态定义:阶段的七种生命态

GSD Phase Runner 定义了七种阶段状态:

stateDiagram-v2
    [*] --> PENDING: 项目初始化
    PENDING --> INITIALIZING: 开始阶段
    INITIALIZING --> RUNNING: 初始化完成
    INITIALIZING --> FAILED: 前置条件失败
    RUNNING --> PAUSED: 遇到检查点
    RUNNING --> COMPLETED: 执行成功
    RUNNING --> PARTIAL: 部分成功
    RUNNING --> FAILED: 执行失败
    RUNNING --> TIMEOUT: 超时
    PAUSED --> RUNNING: 用户确认
    PAUSED --> ABORTED: 用户取消
    COMPLETED --> [*]
    PARTIAL --> [*]
    FAILED --> RETRYING: 自动重试
    TIMEOUT --> RETRYING: 自动重试
    RETRYING --> RUNNING: 重试开始
    RETRYING --> FAILED: 重试次数耗尽
    FAILED --> [*]
    TIMEOUT --> [*]
    ABORTED --> [*]

状态详解:

状态含义可转换至说明
PENDING等待执行INITIALIZING阶段已被计划,但尚未开始
INITIALIZING初始化中RUNNING, FAILED正在加载上下文、检查依赖
RUNNING执行中COMPLETED, PARTIAL, FAILED, TIMEOUT, PAUSEDAgent 正在执行任务
PAUSED暂停RUNNING, ABORTED等待用户输入(检查点)
COMPLETED已完成阶段成功结束
PARTIAL部分完成主要任务完成,但存在警告
FAILED失败RETRYING, —执行过程中发生错误
TIMEOUT超时RETRYING, —超过配置的超时时间
RETRYING重试中RUNNING, FAILED正在准备重新执行
ABORTED已中止用户手动取消

状态的数据结构:

enum PhaseStatus {
  PENDING = 'pending',
  INITIALIZING = 'initializing',
  RUNNING = 'running',
  PAUSED = 'paused',
  COMPLETED = 'completed',
  PARTIAL = 'partial',
  FAILED = 'failed',
  TIMEOUT = 'timeout',
  RETRYING = 'retrying',
  ABORTED = 'aborted',
}

interface PhaseState {
  phaseId: string;
  status: PhaseStatus;
  startTime?: number;        // 首次开始时间
  endTime?: number;          // 结束时间
  retryCount: number;        // 已重试次数
  lastError?: PhaseError;    // 最后一次错误信息
  checkpoint?: Checkpoint;   // 当前检查点(如果有)
  artifacts?: Artifact[];    // 阶段产物
}

3.2 状态转换:事件驱动的状态迁移

Phase Runner 的状态机是事件驱动的。任何状态转换都由一个明确的事件触发,而不是直接修改状态。

事件类型定义:

type PhaseEvent =
  | { type: 'PHASE_START'; phaseId: string }
  | { type: 'INIT_COMPLETE'; phaseId: string }
  | { type: 'INIT_FAILED'; phaseId: string; error: PhaseError }
  | { type: 'AGENT_COMPLETE'; phaseId: string; result: AgentResult }
  | { type: 'AGENT_FAILED'; phaseId: string; error: PhaseError }
  | { type: 'CHECKPOINT_REACHED'; phaseId: string; checkpoint: Checkpoint }
  | { type: 'CHECKPOINT_RESOLVED'; phaseId: string; decision: 'continue' | 'abort' }
  | { type: 'TIMEOUT'; phaseId: string }
  | { type: 'RETRY'; phaseId: string }
  | { type: 'USER_ABORT'; phaseId: string };

状态转换函数:

function transition(
  state: PhaseState,
  event: PhaseEvent
): PhaseState {
  // 状态转换表:当前状态 + 事件 => 下一状态
  const transitions: Record<PhaseStatus, Partial<Record<PhaseEvent['type'], PhaseStatus>>> = {
    [PhaseStatus.PENDING]: {
      'PHASE_START': PhaseStatus.INITIALIZING,
    },
    [PhaseStatus.INITIALIZING]: {
      'INIT_COMPLETE': PhaseStatus.RUNNING,
      'INIT_FAILED': PhaseStatus.FAILED,
    },
    [PhaseStatus.RUNNING]: {
      'AGENT_COMPLETE': PhaseStatus.COMPLETED,
      'AGENT_FAILED': PhaseStatus.FAILED,
      'TIMEOUT': PhaseStatus.TIMEOUT,
      'CHECKPOINT_REACHED': PhaseStatus.PAUSED,
      'USER_ABORT': PhaseStatus.ABORTED,
    },
    [PhaseStatus.PAUSED]: {
      'CHECKPOINT_RESOLVED': PhaseStatus.RUNNING,  // 继续
      'USER_ABORT': PhaseStatus.ABORTED,           // 取消
    },
    [PhaseStatus.FAILED]: {
      'RETRY': PhaseStatus.RETRYING,
    },
    [PhaseStatus.TIMEOUT]: {
      'RETRY': PhaseStatus.RETRYING,
    },
    [PhaseStatus.RETRYING]: {
      'PHASE_START': PhaseStatus.RUNNING,
    },
    // 终态不接受任何事件
    [PhaseStatus.COMPLETED]: {},
    [PhaseStatus.PARTIAL]: {},
    [PhaseStatus.ABORTED]: {},
  };

  const nextStatus = transitions[state.status]?.[event.type];

  if (!nextStatus) {
    throw new InvalidTransitionError(
      `Cannot transition from ${state.status} via ${event.type}`
    );
  }

  return {
    ...state,
    status: nextStatus,
    // 根据事件类型更新附加字段
    ...(event.type === 'AGENT_FAILED' && { lastError: event.error }),
    ...(event.type === 'TIMEOUT' && { lastError: { type: 'timeout', message: 'Phase timed out' } }),
    ...(event.type === 'RETRY' && { retryCount: state.retryCount + 1 }),
  };
}

设计要点:

  • 不可变更新:状态转换函数是纯函数,接收旧状态返回新状态,不修改原状态对象。这保证了状态历史的可追溯性。
  • 非法转换拦截:如果某个事件在当前状态下不被允许(例如在 COMPLETED 状态下收到 AGENT_FAILED),Phase Runner 会抛出 InvalidTransitionError,而不是静默忽略。
  • 终态不可变COMPLETEDPARTIALABORTED 是终态,一旦进入就不能再转换。这防止了「已完成的阶段被重新打开」这种逻辑错误。

3.3 事件驱动架构:观察者模式与异步队列

Phase Runner 的事件系统采用**发布-订阅(Pub/Sub)**模式实现。状态机是核心订阅者,但其他模块(如日志系统、UI 渲染器、外部监控系统)也可以订阅事件。

事件总线架构:

flowchart TD
    subgraph 事件生产者
        Init["阶段初始化器"]
        Agent["Agent 调度器"]
        Timer["超时定时器"]
        User["用户输入"]
    end

    subgraph 事件总线
        EB["EventBus
异步队列"] end subgraph 事件消费者 SM["状态机
State Machine"] Logger["日志系统"] UI["UI 渲染器"] Audit["审计记录器"] Ext["外部钩子"] end Init -->|"PHASE_START"| EB Agent -->|"AGENT_COMPLETE"| EB Timer -->|"TIMEOUT"| EB User -->|"USER_ABORT"| EB EB --> SM EB --> Logger EB --> UI EB --> Audit EB --> Ext

事件队列的异步处理:

class PhaseEventBus {
  private queue: PhaseEvent[] = [];
  private handlers: Map<string, Set<(event: PhaseEvent) => void>> = new Map();
  private processing = false;

  emit(event: PhaseEvent): void {
    this.queue.push(event);
    if (!this.processing) {
      this.processQueue();
    }
  }

  private async processQueue(): Promise<void> {
    this.processing = true;

    while (this.queue.length > 0) {
      const event = this.queue.shift()!;
      const handlers = this.handlers.get(event.type) || new Set();

      // 串行执行所有处理器,保证顺序一致性
      for (const handler of handlers) {
        try {
          await handler(event);
        } catch (error) {
          // 处理器错误不应影响其他处理器
          console.error(`Event handler failed for ${event.type}:`, error);
        }
      }
    }

    this.processing = false;
  }

  on(eventType: string, handler: (event: PhaseEvent) => void): void {
    if (!this.handlers.has(eventType)) {
      this.handlers.set(eventType, new Set());
    }
    this.handlers.get(eventType)!.add(handler);
  }
}

设计要点:

  • 串行处理:虽然事件总线内部使用异步队列,但同一类型的事件是串行处理的。这避免了并发状态转换导致的竞态条件。
  • 错误隔离:单个事件处理器的失败不会阻止其他处理器执行。这保证了日志记录、UI 更新等辅助功能不会因为主逻辑错误而中断。
  • 背压控制:事件队列有最大长度限制(默认 1000)。如果队列溢出,新事件会被丢弃并记录警告。这防止了内存泄漏。

四、生命周期管理:钩子和异常控制

生命周期管理是 Phase Runner 的「安全带」机制。它通过阶段钩子、错误处理和超时控制,确保阶段执行过程的可观测性、可恢复性和安全性。

4.1 阶段生命周期钩子:在关键节点插入自定义逻辑

Phase Runner 提供了一套类似前端框架的生命周期钩子,允许用户和外部扩展在阶段执行的关键节点插入自定义逻辑。

钩子体系:

flowchart TD
    subgraph "Phase Runner 生命周期钩子"
        direction TB

        A["beforePhaseStart
阶段开始前"] --> B["afterInit
初始化完成后"] B --> C["beforeAgentCall
Agent 调用前"] C --> D["onAgentStream
Agent 流式输出中"] D --> E["afterAgentCall
Agent 调用后"] E --> F["beforeStateUpdate
状态更新前"] F --> G["afterPhaseComplete
阶段完成后"] H["onError
错误发生时"] -.-> E I["onTimeout
超时发生时"] -.-> E J["onCheckpoint
检查点触发时"] -.-> C end

钩子接口定义:

interface PhaseHooks {
  // 阶段开始前
  beforePhaseStart?: (ctx: PhaseRuntimeContext) => Promise<void>;

  // 初始化完成后(Agent 调用前)
  afterInit?: (ctx: PhaseRuntimeContext) => Promise<void>;

  // Agent 调用前(可用于修改 prompt)
  beforeAgentCall?: (
    ctx: PhaseRuntimeContext,
    prompt: string
  ) => Promise<string>;  // 返回修改后的 prompt

  // Agent 流式输出中(实时回调)
  onAgentStream?: (
    ctx: PhaseRuntimeContext,
    chunk: string
  ) => Promise<void>;

  // Agent 调用后(结果处理前)
  afterAgentCall?: (
    ctx: PhaseRuntimeContext,
    result: AgentResult
  ) => Promise<AgentResult>;  // 可修改结果

  // 状态更新前(可拦截或修改状态变更)
  beforeStateUpdate?: (
    ctx: PhaseRuntimeContext,
    stateUpdate: Partial<ProjectState>
  ) => Promise<Partial<ProjectState>>;

  // 阶段完成后(无论成功失败)
  afterPhaseComplete?: (ctx: PhaseRuntimeContext) => Promise<void>;

  // 错误发生时
  onError?: (
    ctx: PhaseRuntimeContext,
    error: PhaseError
  ) => Promise<'retry' | 'abort' | 'continue'>;

  // 超时发生时
  onTimeout?: (
    ctx: PhaseRuntimeContext
  ) => Promise<'retry' | 'abort'>;

  // 检查点触发时
  onCheckpoint?: (
    ctx: PhaseRuntimeContext,
    checkpoint: Checkpoint
  ) => Promise<string>;  // 返回用户决策
}

钩子的使用场景:

钩子典型使用场景
beforePhaseStart清理临时文件、检查磁盘空间、加载自定义配置
afterInit注入额外的参考文档、修改阶段上下文
beforeAgentCallPrompt 注入(如添加自定义指令)、A/B 测试不同 prompt
onAgentStream实时渲染 Markdown、计算 Token 使用量、检测敏感信息
afterAgentCall结果过滤(去除 PII)、自动格式化代码、质量评分
beforeStateUpdate拦截危险操作(如删除关键文件)、添加自定义元数据
afterPhaseComplete发送通知(Slack/邮件)、生成阶段报告、触发 CI/CD
onError自动创建 issue、通知运维、记录到错误追踪系统
onTimeout优雅降级(切换到低精度模型)、保存检查点
onCheckpoint自动化审查(用另一个 Agent 审查结果)、批量确认

钩子的注册方式:

// 方式一:在 config.json 中注册(全局钩子)
{
  "hooks": {
    "beforePhaseStart": "./hooks/before-start.js",
    "onError": "./hooks/error-handler.js"
  }
}

// 方式二:在阶段定义中注册(阶段级钩子)
// workflows/execute-phase.md
---
hooks:
  beforeAgentCall: ./hooks/inject-custom-prompt.js
  onAgentStream: ./hooks/real-time-renderer.js
---

// 方式三:SDK 编程式注册(运行时动态)
const runner = new PhaseRunner(config);
runner.hooks.beforePhaseStart = async (ctx) => {
  console.log(`Starting phase: ${ctx.phaseId}`);
};

4.2 错误处理:分层防御与优雅降级

Phase Runner 的错误处理采用分层防御策略,每一层负责处理特定类型的错误。

错误分层架构:

flowchart TD
    subgraph 错误处理分层
        direction TB

        L1["L1: 预防层
Prevention"] L2["L2: 捕获层
Catch"] L3["L3: 恢复层
Recovery"] L4["L4: 报告层
Reporting"] end L1 -->|"前置条件检查
参数验证
类型守卫"| L2 L2 -->|"try/catch
Promise.catch
Event error 隔离"| L3 L3 -->|"重试机制
回滚操作
检查点恢复"| L4 L4 -->|"日志记录
STATE.md 标记
用户通知"| End(["错误闭环"])

错误分类与处理策略:

// Phase Runner 的错误体系
class PhaseError extends Error {
  constructor(
    message: string,
    public code: string,           // 错误代码
    public severity: 'fatal' | 'recoverable' | 'warning',
    public recoverable: boolean,   // 是否可恢复
    public context?: Record<string, unknown>
  ) {
    super(message);
  }
}

// 具体错误类型
class PhaseDependencyError extends PhaseError {
  constructor(depPhase: string) {
    super(
      `Dependency phase ${depPhase} not completed`,
      'DEPENDENCY_MISSING',
      'fatal',
      false
    );
  }
}

class PreconditionError extends PhaseError {
  constructor(condition: string) {
    super(
      `Precondition not met: ${condition}`,
      'PRECONDITION_FAILED',
      'fatal',
      false
    );
  }
}

class AgentInvocationError extends PhaseError {
  constructor(cause: Error) {
    super(
      `Agent invocation failed: ${cause.message}`,
      'AGENT_INVOCATION_FAILED',
      'recoverable',
      true
    );
  }
}

class TimeoutError extends PhaseError {
  constructor(timeoutMs: number) {
    super(
      `Phase timed out after ${timeoutMs}ms`,
      'PHASE_TIMEOUT',
      'recoverable',
      true
    );
  }
}

错误处理流程:

async function executePhaseWithErrorHandling(
  phaseId: string,
  ctx: ProjectContext,
  hooks: PhaseHooks
): Promise<PhaseResult> {
  let phaseCtx: PhaseRuntimeContext | undefined;

  try {
    // L1: 预防层 - 参数验证
    validatePhaseId(phaseId);
    validateProjectContext(ctx);

    // 阶段初始化
    phaseCtx = await initializePhase(phaseId, ctx);
    await hooks.beforePhaseStart?.(phaseCtx);

    // 上下文组装
    const prompt = await assembleContext(phaseCtx);
    const modifiedPrompt = await hooks.beforeAgentCall?.(phaseCtx, prompt) ?? prompt;

    // Agent 调用(带超时控制)
    const result = await runWithTimeout(
      () => invokeAgent(modifiedPrompt, phaseCtx!, hooks.onAgentStream),
      phaseCtx.phaseDef.timeout ?? 300000  // 默认 5 分钟
    );

    const processedResult = await hooks.afterAgentCall?.(phaseCtx, result) ?? result;

    // 结果处理
    await processResult(phaseCtx, processedResult);

    // 状态更新
    const stateUpdate = buildStateUpdate(phaseCtx, processedResult);
    const finalUpdate = await hooks.beforeStateUpdate?.(phaseCtx, stateUpdate) ?? stateUpdate;
    await updateState(ctx.statePath, finalUpdate);

    await hooks.afterPhaseComplete?.(phaseCtx);

    return { success: true, phaseId };

  } catch (error) {
    // L2: 捕获层
    const phaseError = normalizeError(error);

    // L3: 恢复层 - 尝试恢复
    if (phaseError.recoverable && phaseCtx) {
      const decision = await hooks.onError?.(phaseCtx, phaseError) ?? 'abort';

      if (decision === 'retry' && phaseCtx.retryCount < MAX_RETRIES) {
        phaseCtx.retryCount++;
        await emitEvent({ type: 'RETRY', phaseId });
        return executePhaseWithErrorHandling(phaseId, ctx, hooks);  // 递归重试
      }

      if (decision === 'continue') {
        // 标记为 PARTIAL 并继续
        await updateState(ctx.statePath, {
          phaseId,
          status: PhaseStatus.PARTIAL,
          lastError: phaseError,
        });
        return { success: true, phaseId, partial: true };
      }
    }

    // L4: 报告层 - 记录错误并终止
    if (phaseCtx) {
      await updateState(ctx.statePath, {
        phaseId,
        status: PhaseStatus.FAILED,
        lastError: phaseError,
      });
      await hooks.afterPhaseComplete?.(phaseCtx);
    }

    logger.error(`Phase ${phaseId} failed:`, phaseError);
    throw phaseError;
  }
}

4.3 超时控制:防止无限等待的保险丝

在 AI Agent 驱动的开发中,Agent 可能陷入「思考循环」或调用耗时过长的工具。超时控制是防止这种情况的保险丝。

超时机制架构:

sequenceDiagram
    participant PR as Phase Runner
    participant T as 超时定时器
    participant AG as Agent 调度器
    participant LLM as LLM API

    PR->>AG: 启动 Agent 调用
    PR->>T: 启动定时器 (timeout = 5min)
    activate T
    AG->>LLM: 发送请求
    activate LLM

    alt 正常完成
        LLM-->>AG: 响应完成
        AG-->>PR: 返回结果
        PR->>T: 取消定时器
        deactivate T
    else 超时发生
        T-->>PR: TIMEOUT 事件
        PR->>AG: 取消调用
        AG->>LLM: 中断请求
        deactivate LLM
        PR->>PR: 触发 onTimeout 钩子
        alt 用户选择重试
            PR->>PR: 进入 RETRYING 状态
        else 用户选择中止
            PR->>PR: 进入 FAILED 状态
        end
    end

超时控制实现:

async function runWithTimeout<T>(
  fn: () => Promise<T>,
  timeoutMs: number,
  onTimeout?: () => void
): Promise<T> {
  return new Promise((resolve, reject) => {
    const timer = setTimeout(() => {
      reject(new TimeoutError(timeoutMs));
      onTimeout?.();
    }, timeoutMs);

    fn()
      .then((result) => {
        clearTimeout(timer);
        resolve(result);
      })
      .catch((error) => {
        clearTimeout(timer);
        reject(error);
      });
  });
}

可配置的超时策略:

Phase Runner 支持三级超时配置:

级别配置位置说明默认值
全局超时config.jsonglobalTimeout所有阶段的默认超时300000ms (5min)
阶段超时workflows/*.mdtimeout特定阶段的超时继承全局
Agent 超时Agent 定义中的 timeout单次 Agent 调用的超时继承阶段

超时时间的计算方式是累加的:阶段超时 = 初始化时间 + 上下文组装时间 + Agent 调用时间 + 结果处理时间。如果阶段定义中包含多个 Agent 调用(如 Wave 模型),则阶段超时需要覆盖所有 Agent 调用的总和。


五、与 workflows/*.md 的衔接:从声明到执行的桥梁

Phase Runner 与 GSD 工作流体系之间的衔接,是整个框架中最重要的边界之一。理解这个衔接机制,有助于我们在自定义工作流时遵循正确的约定。

5.1 工作流解析流水线

当 Phase Runner 需要执行一个阶段时,它首先通过工作流解析器将 workflows/*.md 文件转化为可执行的结构化配置。

解析流水线:

flowchart LR
    MD["workflows/phase.md
Markdown 文件"] --> FM["Frontmatter
解析器"] FM --> BD["Body
模板引擎"] BD --> VL["变量
解析器"] VL --> CD["条件
求值器"] CD --> PD["PhaseDefinition
结构化配置"]

Frontmatter 解析:

工作流文件的 YAML frontmatter 包含了阶段的元数据配置:

---
phase: execute-phase
name: "Execute Phase"
description: "Execute planned tasks for the current phase"
depends_on:
  - plan-phase
timeout: 600000  # 10 minutes
max_retries: 2
agent: gsd-executor
context_profile: dev
checkpoints:
  - name: pre_commit_review
    condition: "has_uncommitted_changes"
references:
  includes:
    - coding-standards.md
    - testing-guide.md
  max_tokens: 8000
hooks:
  beforeAgentCall: ./hooks/enforce-standards.js
---

Phase Runner 的 frontmatter 解析器会提取这些字段,并将其映射到内部的 PhaseDefinition 接口:

interface PhaseDefinition {
  phase: string;
  name: string;
  description: string;
  dependsOn: string[];
  timeout?: number;
  maxRetries: number;
  agent: string;
  contextProfile?: string;
  checkpoints: CheckpointDef[];
  references: ReferenceConfig;
  hooks: Partial<PhaseHooks>;
  // ... 其他字段
}

5.2 模板变量与运行时绑定

工作流文件的主体部分是一个 Mustache 模板。Phase Runner 在运行时通过**变量绑定(Variable Binding)**将模板转化为最终的 Agent prompt。

绑定过程:

interface VariableBinding {
  // 静态绑定:值在阶段定义或配置中确定
  staticVars: Record<string, string>;

  // 动态绑定:值在运行时从 STATE.md 或环境计算
  dynamicVars: Record<string, () => string>;

  // 惰性绑定:值在首次访问时计算并缓存
  lazyVars: Record<string, () => Promise<string>>;
}

async function bindVariables(
  template: string,
  binding: VariableBinding,
  ctx: PhaseRuntimeContext
): Promise<string> {
  let result = template;

  // 1. 替换静态变量
  for (const [key, value] of Object.entries(binding.staticVars)) {
    result = result.replace(new RegExp(`{{\\s*${key}\\s*}}`, 'g'), value);
  }

  // 2. 替换动态变量
  for (const [key, getter] of Object.entries(binding.dynamicVars)) {
    if (result.includes(`{{${key}}}`)) {
      result = result.replace(new RegExp(`{{\\s*${key}\\s*}}`, 'g'), getter());
    }
  }

  // 3. 替换惰性变量(按需计算)
  for (const [key, getter] of Object.entries(binding.lazyVars)) {
    if (result.includes(`{{${key}}}`)) {
      const value = await getter();
      result = result.replace(new RegExp(`{{\\s*${key}\\s*}}`, 'g'), value);
    }
  }

  // 4. 检查未解析的变量
  const unresolved = result.match(/{{\s*[\w.]+\s*}}/g);
  if (unresolved) {
    logger.warn(`Unresolved variables in phase ${ctx.phaseId}:`, unresolved);
  }

  return result;
}

内置变量清单:

Phase Runner 提供了一组内置变量,在所有阶段模板中均可使用:

变量名来源示例值说明
{{project_name}}config.jsonmy-web-app项目名称
{{current_phase}}STATE.mdexecute-phase当前阶段 ID
{{phase_name}}阶段定义Execute Phase当前阶段名称
{{timestamp}}系统时间2026-04-24T15:00:00ZISO 格式时间戳
{{state_summary}}STATE.md3/5 tasks completed状态摘要
{{git_branch}}Git 工作区feature/gsd-integration当前分支
{{git_commit}}Git 工作区a1b2c3d当前提交 hash

5.3 条件分支与动态编排

GSD 工作流支持简单的条件分支,Phase Runner 在运行时对条件表达式求值,决定是否包含某段模板内容。

条件语法:

{{#if condition}}
这部分内容仅在条件为真时包含
{{/if}}

{{#unless condition}}
这部分内容仅在条件为假时包含
{{/unless}}

条件求值:

async function evaluateCondition(
  condition: string,
  ctx: PhaseRuntimeContext
): Promise<boolean> {
  // 支持的条件表达式
  // 1. 状态检查: "state.current_phase == 'plan-phase'"
  // 2. 文件存在: "file_exists('PLAN.md')"
  // 3. Git 检查: "git.has_uncommitted_changes"
  // 4. 环境检查: "env.CI == 'true'"
  // 5. 复合条件: "A && B || C"

  const evaluators: Record<string, (args: string[]) => boolean> = {
    'state': (args) => getNestedValue(ctx.state, args[0]) === args[1],
    'file_exists': (args) => fs.existsSync(path.resolve(ctx.projectRoot, args[0])),
    'git': (args) => evaluateGitCondition(args[0], ctx.projectRoot),
    'env': (args) => process.env[args[0]] === args[1],
  };

  // 简单的表达式解析器
  return parseAndEvaluate(condition, evaluators);
}

5.4 工作流热重载与缓存

在开发模式下,Phase Runner 支持工作流文件的热重载——当 workflows/*.md 文件被修改时,无需重启进程即可生效。

热重载机制:

flowchart TD
    Watch["文件监视器
fs.watch"] -->|"文件变化"| Invalidate["使缓存失效"] Invalidate --> Reload["重新解析工作流"] Reload --> Validate["验证工作流定义"] Validate -->|"验证通过"| Update["更新运行时缓存"] Validate -->|"验证失败"| Reject["拒绝热重载
保留旧版本"]

Phase Runner 内部维护了一个 WorkflowCache,以 phaseId 为键缓存解析后的 PhaseDefinition。当文件变化时,对应的缓存条目被标记为失效,下次执行该阶段时会重新解析。


六、总结:Phase Runner 的设计哲学

通过本文的拆解,我们可以看到 phase-runner.ts 的 39KB 代码背后蕴含的设计哲学:

6.1 核心设计原则

原则体现收益
Separation of Concerns工作流声明 phase,Phase Runner 执行 phase工作流保持简洁,引擎保持专注
Event-Driven State Machine所有状态转换由事件触发可预测、可追踪、可调试
Fail-Fast依赖不满足立即报错,不静默等待避免死锁和状态漂移
Immutable State Updates状态转换函数返回新状态历史可追溯,便于审计
Graceful Degradation错误分层处理,支持重试和回滚系统韧性高
Context Budget参考文档按需加载,总量可控防止上下文窗口溢出

6.2 Phase Runner 在 GSD 架构中的位置

flowchart TD
    subgraph "GSD 完整架构"
        direction TB

        User["用户"] --> CLI["CLI 层"]
        CLI --> Config["配置系统"]
        Config --> PR["Phase Runner
核心引擎"] PR --> WF["工作流解析器
workflows/*.md"] PR --> AG["Agent 调度器
agents/*.md"] PR --> Git["Git 操作层"] PR --> FS["文件系统层"] PR -.->|"状态更新"| State["STATE.md"] PR -.->|"审计日志"| Audit["audit-trail/"] end

Phase Runner 是 GSD 架构的「心脏」——它接收来自 CLI 的指令,协调工作流、Agent、Git 和文件系统,最终将用户的意图转化为可验证的软件交付成果。理解 Phase Runner 的设计,是掌握 GSD SDK 运行时的关键一步。

6.3 阅读源码的建议

如果你希望进一步深入 phase-runner.ts 的实现细节,建议按以下顺序阅读:

  1. runPhase() 入口开始(约第 200 行):这是阶段执行的主循环,理解它就能把握整体流程。
  2. 关注状态机部分(约第 500-700 行):transition() 函数和 PhaseEventBus 是理解阶段生命周期管理的关键。
  3. 研究上下文组装逻辑(约第 800-1000 行):assembleContext()bindVariables() 是 Phase Runner 最具技术含量的部分。
  4. 查看钩子系统(约第 1200-1400 行):PhaseHooks 接口和 executeHook() 函数展示了扩展机制的设计。


下一篇预告: 第 40 篇《Plan Parser 与 Prompt Builder》

我们将深入解析 GSD SDK 的另外两个核心模块:Plan Parser 如何将 Agent 生成的 PLAN.md 解析为结构化执行计划,以及 Prompt Builder 如何将阶段定义、参考文档和运行时状态组装成高质量的 LLM prompt。敬请期待。