# Phase C Integrator

> 十步循环 Phase C - 集成阶段执行器，编排 C.1-C.2 步骤。 使用场景："执行集成阶段"、"Phase C"、"提交代码并创建 PR"

- Skill: `10cg/phase-c-integrator` (Agent Skill, multi-file: 16 files)
- Install (CLI): `npx skillmds@latest add 10cg/phase-c-integrator`
- Raw SKILL.md: https://api.skillmd.com/api/skills/10cg/phase-c-integrator/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: 10cg (https://skillmd.com/u/10cg)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/10cg/phase-c-integrator

---


# Phase C - 集成阶段 (Integrator)

> **版本**: 1.3.0 | **十步循环**: C.1-C.2
> **更新**: 2026-05-10 - C.2.4 Pre-Merge Precondition Gate (#60, consume aether `--in-flight` primitive)
> **历史更新**: 2026-03-27 - 升级审计触发从 agent-team-audit 改为 audit-engine

## 快速开始

### 我应该使用这个 Skill 吗？

**使用场景**:
- 需要提交代码变更
- 需要创建 Pull Request
- 需要合并分支
- 开发完成后的集成阶段

**不使用场景**:
- 无变更需要提交 → 跳过 C.1
- 不需要 PR → 跳过 C.2

---

## 配置 (config-loader)

执行前读取 `.aria/config.json`，缺失则使用默认值。参见 [config-loader](../config-loader/SKILL.md)。

| 字段 | 默认值 | 说明 |
|------|--------|------|
| `audit.enabled` | `false` | 启用 audit-engine 审计 (新) |
| `audit.checkpoints.pre_merge` | `"off"` | pre_merge 检查点模式 |
| `experiments.agent_team_audit` | `false` | 旧配置 (向后兼容，自动映射到 audit.*) |
| `experiments.agent_team_audit_points` | `["pre_merge"]` | 旧配置 (向后兼容) |
| `upm.milestone_driven` | `false` | 启用 C.2.6 里程碑子进度追加 (opt-in) |
| `phase_c_integrator.pre_merge_gate.enabled` | `true` | 启用 C.2.4 pre-merge precondition gate (v1.3.0+) |
| `phase_c_integrator.submodule_gate.mode` | `"block"` (v1.49.0+ default) / `"warn"` (legacy) / `"off"` (bypass) | C.2.4.5 submodule pointer regression gate 模式 (Spec aria-submodule-pointer-regression-gate) |
| `phase_c_integrator.pre_merge_gate.ci_backends` | `null` (auto-detect) / `[]` (explicit disable) / `[{name: "..."}]` (explicit list) | **v1.31.0+** CI backend 选择 (替代旧 `primitive_preference`). 见 §C.2.4.X CI Backends |
| `phase_c_integrator.pre_merge_gate.no_ci_fallback` | `"skip_with_warning"` | 无可用 CI backend 时降级 (`skip_with_warning` / `abort`). **v1.31.0+** 替代旧 `no_aether_fallback` (alias 仍读, 发 deprecation warning, v2.0 移除) |
| `phase_c_integrator.pre_merge_gate.wait_timeout_seconds` | `1800` | wait+retry max 等待时长 (默认 30 min) |
| `phase_c_integrator.pre_merge_gate.wait_check_intervals` | `[30,60,120,300,300]` | 指数退避秒数; 数组耗尽后重复 `intervals[-1]` |
| `phase_c_integrator.pre_merge_gate.primitive_call_timeout_seconds` | `30` | 单次 aether subprocess 调用 timeout |
| `phase_c_integrator.pre_merge_gate.poll_chunk_seconds` | `5` | Ctrl-C polling chunk 大小 |
| `phase_c_integrator.pre_merge_gate.path_coverage_enabled` | `true` | **v1.65.0+** 路径覆盖感知 (aria-plugin #122): 变更路径结构性无 CI workflow 覆盖时产 `not_applicable`, 跳过 (a) PR CI wait ((b) main in-flight 照查)。见 §C.2.4 步骤 2.5 |
| `phase_c_integrator.pre_merge_gate.no_run_prompt_after_observations` | `3` | **v1.66.5+** (#152) 远端零 run (`pr_ci_status=not_found`) 连续观测达此次数才 prompt; int ≥2, 不提供 1; 非法值 warn + 回落 3 |

当 `audit.enabled=true` 且 `audit.checkpoints.pre_merge != "off"` 时，C.2 合并前触发 audit-engine (pre_merge 检查点)。
旧配置 `experiments.agent_team_audit=true` 且 `"pre_merge" in agent_team_audit_points` 自动映射到新配置。

---

## 核心功能

| 步骤 | Skill | 职责 | 输出 |
|------|-------|------|------|
| C.1 | commit-msg-generator | Git 提交 | commit_sha, message |
| C.2 | branch-manager | PR/合并 | pr_url, pr_number |

---

## 执行流程

### 输入

```yaml
context:
  phase_cycle: "Phase4-Cycle9"
  module: "mobile"
  changed_files: ["lib/auth.dart", "test/auth_test.dart"]
  branch_name: "feature/mobile/TASK-001-add-auth"  # 来自 Phase B
  test_results:                                     # 来自 Phase B
    passed: true
    coverage: 87.5

  # v1.1.0 新增: branch-finisher 输出
  completion_option: 1                              # 来自 branch-finisher
  worktree_path: ".git/worktrees/TASK-001-xxx"     # 可选
  validation_report:                                # 来自 branch-finisher
    passed: true
    blocking_failures: 0
    warnings: 1

config:
  skip_steps: []
  params:
    enhanced_markers: true        # 使用增强提交标记
    create_pr: true               # 是否创建 PR
```

### 步骤执行

```yaml
C.1 - Git 提交:
  skill: commit-msg-generator
  params:
    enhanced_markers: true
    subagent_type: "from_context"
    phase_cycle: "from_context"
    module: "from_context"
  skip_if:
    - no_changes_to_commit: true
  action:
    - 分析暂存区变更
    - 生成规范提交消息
    - 执行 git commit
  output:
    commit_sha: "abc1234"
    commit_message: "feat(auth): 添加用户认证..."

C.2 - PR/合并:
  skill: branch-manager
  action: pr
  skip_if:
    - no_pr_needed: true
    - direct_push_allowed: true
  pre_hook:                               # audit-engine 检查点
    audit_engine:
      checkpoint: pre_merge
      步骤:
        1. 通过 config-loader 读取 .aria/config.json audit 块
        2. 检查 audit.enabled — false 则跳过，保持现有行为不变
        3. 检查 audit.checkpoints.pre_merge — "off" 则跳过
        4. 如启用: 调用 audit-engine
           - checkpoint: "pre_merge"
           - mode: 来自配置 (convergence / challenge / adaptive)
           - context: PR diff (branch_name vs base)
        5. 处理 verdict:
           - PASS / PASS_WITH_WARNINGS → 继续推送和创建 PR
           - FAIL → 阻塞合并，输出审计报告
      backward_compat:
        audit.enabled=false: 完全跳过，Phase C 行为与之前完全相同
        旧配置 experiments.agent_team_audit: 由 audit-engine 内部映射处理
      fallback_description: |
        audit-engine 内部通过 agent-team-audit 单轮引擎执行审计。
        直接调用 agent-team-audit 已由 audit-engine 编排层取代。
      on_fail: 阻塞合并, 输出审计报告
      on_skip: 继续合并 (审计未启用)
  action:
    - (如 audit.enabled) 触发 audit-engine (pre_merge 检查点)
    - 推送分支到远程
    - 创建 Pull Request
    - (可选) 自动合并
  output:
    pr_url: "https://..."
    pr_number: 123
    audit_verdict: "PASS"                 # 如审计启用 (PASS | PASS_WITH_WARNINGS | FAIL)
    audit_report: ".aria/audit-reports/pre_merge-{timestamp}.md"

> **注意**: branch-manager 会自动处理 Cloudflare Access 配置。
> 统一规范见 `../forgejo-sync/PRE_CHECK.md`

C.2.4 - Pre-Merge Precondition Gate (v1.3.0+):
  触发条件:
    - C.2 action PR 已创建 (PR_NUMBER + PR_URL 已知)
    - 即将调用 branch-manager merge action (auto_merge=true 或 user-triggered continue)
    - 配置 phase_c_integrator.pre_merge_gate.enabled: true (默认)
  primitive 调用:
    - aether ci status --branch <MAIN_BRANCH> --in-flight --json (查主干是否有 in-flight)
      ⚠️ <MAIN_BRANCH> 取本项目主干的真实名字 — **本项目是 `master`, 不是 `main`**。
      照抄字面 main 正是 aria-plugin #137: 查一个不存在的分支返回「没有在跑的」⇒ 恒放行。
    - aether ci status --branch <PR_BRANCH> --json (查本 PR CI 状态)
    - aria 端 verdict 计算 (aether-pre-merge-check skill 从未实施, P0-B not shipped)
  三态结果:
    green:  本 PR CI passing + main 无 in-flight CI → 继续 branch-manager merge
            (v1.65.0+ 亦含: path coverage not_applicable + main 无 in-flight, 带警告放行)
    wait:   main 有 in-flight CI run OR PR CI pending → 进入 wait+retry (workflow-runner wait_recoverable) OR PR CI not_found (远端零 run, v1.66.5+ #152)
    fail:   PR CI failing OR primitive 错误 → BLOCK + 报告
  output:
    pre_merge_verdict: "green" | "wait" | "fail"
    in_flight_runs: [{run_id, branch, started_at, elapsed_seconds}]   # wait 时
    pr_ci_status: "passing" | "failing" | "pending" | "not_applicable" | "not_found"   # not_applicable = gate 层产生 (v1.65.0+); not_found = backend 层零 run, v1.66.5+
    primitive_used: "aether-ci-cli" | "manual"
    primitive_version_sha: "f29abee"   # aether-cli #116 baseline
    path_coverage: {decision, workflows_scanned, matched_workflows, changed_files_count, reason, dispatchable_workflows}   # v1.65.0+, 评估执行时在场; dispatchable_workflows v1.66.5+
    gate_error: {kind, message, prompt_after_observations?}   # v1.66.5+ 三类在场, 见 Output schema

C.2.4.5 - Submodule Pointer Regression Gate (v1.28.0+):
  触发条件:
    - C.2.4 verdict=green (CI gate 已通过)
    - 即将调用 branch-manager merge action
    - 配置 phase_c_integrator.submodule_gate.mode: "block" (v1.49.0+ default) | "warn" (legacy opt-out) | "off" (emergency bypass)
  primitive 调用:
    - git fetch origin (bare, 更新所有 ref) — 强制, 失败 abort
    - git -C <submodule> fetch origin — 每 submodule
    - git -C <submodule> merge-base --is-ancestor MASTER_PTR FEATURE_PTR — 主 ancestry 检查
    - 双向 ancestry 区分 regression (case c) vs divergence (case d)
  三态结果:
    pass:   所有 submodule pointer 是 forward bump 或 no-change 或 first-time
    block:  至少一个 submodule pointer 是 regression 或 divergence (v1.49.0+ default; v1.28.0 was warn-only)
    bypass: per-PR commit trailer `Submodule-Rollback: ...` 或 PR label `submodule-rollback-approved` 允许 + audit log
  output:
    gate_verdict: "pass" | "block" | "warn" | "bypass"
    affected_submodules: [{path, master_sha, feature_sha, verdict, override}]
    telemetry_written: <metrics file path>

C.2.5 - Multi-Remote Push Enforcement:
  触发条件:
    - Phase C.2 合并成功 (master 已 fast-forward)
    - 配置 phase_c_integrator.multi_remote_push.enabled: true (默认)
  skill: git-remote-helper (降级: 内联实现)
  action: 见下方 ### C.2.5 详细说明

C.2.6 - UPM Milestone Sub-progress Append (optional):
  触发条件:
    - C.2.5 multi-remote push 已完成 (或跳过)
    - 配置 upm.milestone_driven: true (默认 false，opt-in)
    - 当前 commit 与某 User Story 关联 (commit message 含 US-XXX 或 spec change_id)
  action: 见下方 ### C.2.6 详细说明
  backward_compat: upm.milestone_driven=false 时完全跳过，Phase C 行为与之前完全相同
```

### C.2.4 Pre-Merge Precondition Gate (v1.3.0+)

> **新增于 v1.3.0** — 修复 Forgejo Issue #60 "phase-c-integrator 缺少 pre-merge safety gate"。
> 源于 2026-05-02 SilkNode PR-321 cancel PR-322 main CI Run #3161 (459s 部署观测丢失)。
> Consume aether `--in-flight` primitive (aether-cli #116, SHA `f29abee` 2026-05-06)。

**Naming 命名空间澄清**: phase-c-integrator-level 子步骤标签 (C.2.x) 为 **orchestrator-tier**,与 branch-manager 内部实现层 (也用 C.2.x sequence: C.2.1 sync / C.2.2 push / C.2.3 create-PR / **C.2.4 wait-approval** / C.2.5 merge) 是**独立 label namespace**。本 SKILL §C.2.4 = "pre-merge precondition gate" (orchestrator);branch-manager 内部 C.2.4 = "等待审批" (implementation);同名不同 tier,语义独立。

**触发条件**:
- Phase C.2 action PR 已创建 (PR_NUMBER + PR_URL 已知)
- 即将调用 branch-manager merge action (`auto_merge=true` 自动 / `auto_merge=false` user-triggered)
- 配置 `phase_c_integrator.pre_merge_gate.enabled: true` (默认)

**与 branch-manager 边界** (不重叠):
| Skill | 职责 | 调用顺序 |
|-------|------|----------|
| branch-manager (C.2.1-C.2.3) | sync rebase + push branch + create PR | gate 之前 |
| phase-c-integrator C.2.4 | pre-merge gate 三态判定 | gate (本段) |
| branch-manager (C.2.4-C.2.5) | wait approval + merge API call | gate green 后 |

> 📌 **改这段代码前先读**: [`references/pre-merge-gate-empirical-traps.md`](./references/pre-merge-gate-empirical-traps.md)
> —— 一页, 11 条实测踩出来的坑 (§一~§五 7 条 + §六 4 条 零 run 折叠, aria-plugin#152; 活体证据行不计) (ls-remote 零命中返 rc=0 · 参数被当 glob · `UnicodeDecodeError` 不是
> `OSError` 子类 · 核验插入点 · 测试隔离接缝 …)。每一条都不能靠读代码想出来。

**执行流程**:

1. **Aether binary pre-flight check**: `aether --help | grep -q "in-flight"` 验证 binary 含 P0-A flag,缺失 → fail-fast 提示 "请升级 aether ≥ commit f29abee (2026-05-06)"
2. **Backend resolution** (v1.31.0+): `resolve_ci_backend(cfg)` 按 config 显式 `ci_backends` 顺序探测,或 fallback 到 BACKENDS list 静态顺序 (Aether-first, GHA-stub-second);所有 backend probe=False → 按 `no_ci_fallback` 配置降级。详见 §C.2.4.X CI Backends
2.2. **分支存在性核验: main 恒查 (#137); PR 仅 `pr_ci_status=not_found` 时查 (#152, 第七个早退 `pr-branch-not-found` → fail)**: 在上面两道早退与 precheck 之后、path coverage 评估之前, 先确认 `main_branch` 在目标 remote 上**确实存在** (无条件执行)。PR 分支存在性核验则延后 —— 仅当 (a) 腿 `pr_ci_status` 返回 `not_found` (远端零 run) 时才补一次 (第七个早退, 见步骤 5)。判据是**解析出的 ref 名列表中的精确字符串比对**, ⛔ 不看退出码 (零命中亦返 0), ⛔ 不用 glob/pattern (会把 `mast*` 当成命中 `master`)。main 不存在 → `verdict=fail` + `gate_error.kind=main-branch-not-found`; 核验本身没做成 (远端不可达等) → `main-branch-verify-failed`; PR 分支 (仅 `not_found` 后核验) 不存在 → `verdict=fail` + `gate_error.kind=pr-branch-not-found`; PR 侧核验本身没做成则不早退, 继续 wait, 但 message 末尾附加核验失败详情。以上均不可混。**为什么必须有这一步**: backend 结构上无法区分「分支不存在」与「分支没有正在跑的构建」, 两者都返空 runs ⇒ 判 green ⇒ 主干名写错时 Rule #8 这条腿恒真、等于不存在 (本项目主干是 `master` 而缺省值是 `main`, #137 即此); PR 侧同理 (#152 F5): 「PR 分支不存在」与「存在但零 run」在 backend 出口逐字节同形

2.5. **Path coverage 评估** (v1.65.0+, aria-plugin #122; `path_coverage_enabled=true` 默认): `evaluate_path_coverage(main_branch, pr_branch)` 解析仓内 workflow (`.forgejo/.gitea/.github` 三目录) 的 `on:` push/pull_request 触发 paths, 与 `git diff --name-only --no-renames <main>...<pr>` 变更集求交, 三值 decision: `covered` (会/无法排除会触发) / `not_applicable` (高置信零覆盖) / `unknown` (评估失败)。**fail-toward-covered**: 一切不确定 (解析失败 / git 失败 / 未建模构造) → 行为退回现状。**执行上下文契约**: 在执行 C.2 合并的目标仓根内调用 (子模块合并 → 子模块根); `main_branch` 显式传真值 (本项目 `master`), 不依赖 CLI default
3. **Query main in-flight**: `aether ci status --branch <MAIN_BRANCH> --in-flight --json` → parse `data.runs[]` (⚠️ `<MAIN_BRANCH>` 取本项目主干真实名字 —— **本项目是 `master`**; 照抄字面 `main` 即 #137: 查不存在的分支返回「没有在跑的」⇒ 恒放行。步骤 2.2 的核验就是为了让这种错当场暴露) — **无条件执行, 不因 not_applicable 免除** ((b) 轴保留; stub backend 的 NotImplementedError 亦经此步照常 propagate)
4. **Query PR CI status**: `aether ci status --branch <PR_BRANCH> --json` → parse 最近 run 的 `status` 字段 → 映射为 `passing` / `failing` / `pending` / `not_found` (零 run, v1.66.5+ #152)。**v1.65.0+: decision=not_applicable 时本步跳过** (零覆盖路径不存在可等的 PR CI), `pr_ci_status` 置 `not_applicable`
5. **Verdict 计算** (aria 端):
   - `pr_ci_status in [failing, error]` → `verdict=fail`
   - `pr_ci_status == pending` → `verdict=wait` (PR CI 尚未完成)
   - `pr_ci_status == not_found` → `verdict=wait` + `gate_error.kind=no-run-for-branch` (不论 main in-flight; v1.66.5+)
   - PR 分支在 remote 不存在 (`not_found` 后核验) → `verdict=fail` + `gate_error.kind=pr-branch-not-found`
   - `pr_ci_status == not_applicable AND main_in_flight_runs == []` → `verdict=green` + raw_message 留痕 (v1.65.0+)
   - `pr_ci_status == not_applicable AND main_in_flight_runs != []` → `verdict=wait` (仅 (b) 轴驱动, v1.65.0+)
   - `pr_ci_status == passing AND main_in_flight_runs == []` → `verdict=green`
   - `pr_ci_status == passing AND main_in_flight_runs != []` → `verdict=wait`
6. **路由决策**:
   - `green` → 调用 branch-manager merge action,进入 C.2.5。**v1.65.0+ surface 义务 (二者缺一不可)**: (a) green 来源为 not_applicable 时, AI **必须**在 workflow report 加警告行「C.2.4: 变更路径无 CI workflow 覆盖, PR CI wait 已跳过 (not_applicable), main in-flight 已核」; (b) `path_coverage.decision == unknown` 时 (gate 行为=现状, 但评估器自身失败), AI **必须** surface「C.2.4 path coverage 评估失败 (reason=`git-diff-failed` / `workflow-parse-failed` / **`internal-error`**), 已按 covered 现状行为处理」— 评估器静默失效是本机制自己要防的恒红病, 不得吞。**v1.65.3+ (#126)**: `internal-error` 表示**评估器自身异常** (非 git 问题、非 workflow 解析问题), 文案须点明「请报 issue」—— 它与另两个 reason 的排查方向完全不同, 混为一谈会把人引向 git 与 main ref
   - `wait` → 输出 `wait_recoverable` 错误给 workflow-runner,触发 wait+retry 循环 (见 workflow-runner SKILL.md §wait_recoverable)。`gate_error.kind == no-run-for-branch` 时 AI **必须** surface `gate_error.message` 原文 (不得只写「CI pending, 等待中」); 处方段 (**本文唯一定义处**, workflow-runner §wait_recoverable Exit condition 2.5 只引用不复制):

     > 🔴 C.2.4: `<gate_error.message 原文>`。已连续 `<record.no_run_observations>` 次观测到零 run (~`<record.elapsed_seconds>`s)。处方 (择一, 由你执行; AI 不自动执行):
     > (a) dispatch 命令行 — 已由 gate 渲染进 `gate_error.message` (trigger-matched 档, 受 `DISPATCH_VIABLE` 常量与 `dispatchable_workflows` 非空控制), AI 只填 `<owner>/<repo>`; message 无此行则 (a) 不出现。⚠️ 活体实测一次 dispatch 产生成对 run 且 `started_at` 相同 (见 references/pre-merge-gate-empirical-traps.md §六), gate 取最近 run 时 tie 可能读到 failure — 人核按 run id / 状态综合看
     > (b) 推一个碰 CI 触发路径的实质 commit 到 `<pr_branch>` (`workflow-trigger-matched`: matched workflow 声明的 paths; `workflow-files-changed`: 被改 workflow 自己声明的 paths) — 第二次 push 是普通 diff, paths 正常评; 若 workflow 有 `branches` 过滤且不含本分支, 推 commit 无效 → 改分支名或走 (a)/(c) (path_coverage 不建模 branches, 人核) [`unknown` / `empty-diff` / pc=None 档不出现本行]
     > (c) 继续等待 (`continue`) / 放弃 (`abort`)

     prompt 由 workflow-runner §wait_recoverable Exit condition 2.5 按观测计数触发; 交互式直调 §C.2.4 (无 workflow-runner) 时**无计数**, 读者自行按 message 与上方处方处置。
   - `fail` → BLOCK + 输出 verdict + raw_message,phase-c-integrator return failure

**Subprocess 调用规范**:
- `subprocess.run(..., timeout=primitive_call_timeout_seconds)` 强制 (默认 30s)
- timeout 触发 → max 3 attempts retry (backoff 5s/15s/45s) → 仍超时则 `fail` verdict
- exit-code 映射 (per-backend, Aether 示例): `0` = success / `1-126` = aether 错误 → `fail` / `127` = binary not found → `no_ci_fallback` / `-SIGTERM` = subprocess timeout → retry → 仍失败则 `fail`。**NIE-propagation 例外 (v1.31.0+, Hard Constraint #7)**: stub backend (e.g. GHA v1.31.0) query 方法 raise `NotImplementedError` → gate **abort** (raise to caller),**不**走 `no_ci_fallback`

**Helper 实现**: `${ARIA_PLUGIN_ROOT:-aria}/skills/phase-c-integrator/scripts/pre_merge_gate.py` (stdlib + subprocess only)

**Output schema**:
```json
{
  "verdict": "green" | "wait" | "fail",
  "pr_ci_status": "passing" | "failing" | "pending" | "not_applicable" | "not_found",
  "in_flight_runs": [
    {"run_id": 3161, "branch": "main", "started_at": "2026-05-09T12:45:00Z", "elapsed_seconds": 459}
  ],
  "primitive_used": "aether-ci-cli",
  "primitive_version_sha": "f29abee",
  "raw_message": "...",
  "path_coverage": {"decision": "covered|not_applicable|unknown", "workflows_scanned": 1, "matched_workflows": [], "changed_files_count": 2, "reason": "no-triggering-paths", "dispatchable_workflows": []},
  "gate_error": {"kind": "main-branch-not-found|main-branch-verify-failed|pr-branch-not-found|no-run-for-branch", "message": "...", "prompt_after_observations": 3}
}
```

枚举归层注记 (v1.65.0+): `not_applicable` 由 **gate 层**产生 (path coverage 短路, 从不来自 backend); backend `CIStatus.state` Literal 另含 `not_found` (backend 层值, 远端零 run; **v1.66.5+ 起 gate 输出会产生**, 见下方 `gate_error.kind` 消歧表, 列此消除文档与 `ci_backends/base.py:29` 的历史漂移)。`path_coverage` 为 additive 可选键 — 仅评估已执行且流程走到最终 verdict 路径时在场; 各早退分支 (no-backend / precheck 失败 / backend query 失败 / enabled:false) 保持六键不变。

`gate_error` 为 additive 可选键 (aria-plugin #137 / #152) — 三类在场: (i) main 分支存在性核验判 fail (`main-branch-not-found` / `main-branch-verify-failed`, `verdict=fail`, 无 `path_coverage`); (ii) PR 分支存在性核验判 fail (`pr-branch-not-found`, `verdict=fail`, `path_coverage` 在场 ⇔ `path_coverage_enabled`); (iii) `pr_ci_status=not_found` 的 wait 态 (`no-run-for-branch`, `verdict=wait`, 同上, 带 `prompt_after_observations` —— 仅此类带该字段)。`kind` 是**封闭集 4 项**, 四个都是**副本通道**: 同一段文字必定同时写进 `raw_message` (主通道), 只读 `raw_message` 的消费方不丢信息。⚠️ 这些早退分支都**不保持六键不变** (多一个 `gate_error`)。

`kind` 二维消歧表 (分支存在性 × run 存在性; `not_found` 为 backend 态·主语是 *run*, `*-branch-not-found` 为 `kind`·主语是*分支*, 二者同词根异义):

| | run 存在 (有 run) | run 不存在 (零 run, 含存在性未知/核验失败) |
|---|---|---|
| 分支存在 | (无 `gate_error`, 正常 passing/pending/failing 路径) | `no-run-for-branch` (`verdict=wait`; 存在性未知/核验失败归此象限, `message` 末尾带附注后缀) |
| 分支不存在 | — (结构不可达) | `main-branch-not-found` / `pr-branch-not-found` (`verdict=fail`) |

**配置参数**:
| 参数 | 默认 | 说明 |
|------|------|------|
| `enabled` | `true` | gate 总开关 (false → 完全跳过 C.2.4,向后兼容) |
| `ci_backends` | `null` (auto-detect) / `[]` (disable) / `[{name: "..."}]` | **v1.31.0+** backend 选择. Alias: 旧 `primitive_preference` 仍读 + deprecation warning |
| `no_ci_fallback` | `"skip_with_warning"` | 无可用 backend 时 (`skip_with_warning` / `abort`). Alias: 旧 `no_aether_fallback` 仍读 + deprecation warning |
| `wait_timeout_seconds` | `1800` | wait+retry max (默认 30 min) |
| `wait_check_intervals` | `[30,60,120,300,300]` | 指数退避 (秒); 数组耗尽后重复 `intervals[-1]` |
| `primitive_call_timeout_seconds` | `30` | 单次 subprocess 调用 timeout |
| `poll_chunk_seconds` | `5` | Ctrl-C polling chunk |
| `path_coverage_enabled` | `true` | **v1.65.0+** 路径覆盖感知开关 (#122, owner sign-off 2026-07-27 单独批默认 true)。false → 不评估, 输出无 `path_coverage` 键, 行为=v1.64.x |
| `no_run_prompt_after_observations` | `3` | **v1.66.5+** (#152) 远端零 run (`pr_ci_status=not_found`) 连续观测达此次数才 prompt; int ≥2, 不提供 1; 非法值 warn + 回落 3 |

**降级行为**:
- `enabled: false` → 完全跳过 C.2.4 (与 v1.2.0 行为 100% 一致)
- 无可用 backend (所有 backend probe=False, e.g. Aether 未装 + GHA 未 authed) AND `no_ci_fallback: skip_with_warning` → 跳过 + workflow report 警告
- 无可用 backend AND `no_ci_fallback: abort` → BLOCK + 提示安装支持的 CI backend
- 显式禁用 (`ci_backends: []`) → 视为"无可用 backend" 路径,按 `no_ci_fallback` 降级 (canonical way to disable v1.31.0+)
- Stub backend NIE (e.g. `gh` 装但 GHA stub query 未实现) → **不走 fallback,直接 abort** (Hard Constraint #7)
- aether binary 过期 (无 `--in-flight` flag) → fail-fast,**不**继续执行 (避免 silent skip)
- **Path coverage fail-toward-covered 边界 (v1.65.0+)**: 评估的任何不确定面 — git diff 失败 / workflow YAML 解析失败 / 未建模 glob 语法 (`[abc]`/`!` 等, 判匹配) / 未建模触发键 (`pull_request_target` 等, 判 covered; 零贡献仅精确白名单 `{workflow_dispatch, schedule}`) / `paths-ignore` 在场 — 一律落 `covered`/`unknown`, gate 行为与 v1.64.x 逐字段一致; `not_applicable` 只在全部 workflow 解析成功且确定不触发时产生。判定规则 1-8 全分割 + reason 封闭集详见 Spec `openspec/changes/phase-c-gate-path-coverage-not-applicable/proposal.md §1`

**Race condition 处理**: gate 检查与 merge call 之间,main 可能新触发 CI run。窗口最小化 (gate green 后立即调 merge),不消除 race。深度 mitigation 留 future Spec。

---

### C.2.4.X CI Backends (v1.31.0+)

> **新增于 v1.31.0** — 实施 Spec [`aria-ci-backend-abstraction`](../../../openspec/changes/aria-ci-backend-abstraction/proposal.md) (Approved 2026-05-28, post_spec R2 CONVERGED unanimous PASS_WITH_WARNINGS × 3)。源于 2026-05-27 boundary audit P0 C5+C6 — pre_merge_gate.py 去 Aether-only 假设。

**Backend 抽象**: `pre_merge_gate.py` 通过 `aria/skills/phase-c-integrator/scripts/ci_backends/` 包提供的 `CIBackend` ABC + `CIStatus` / `InFlightStatus` dataclass 调用 CI primitive。每个 backend 实现两个 abstract method (`query_pr_ci` / `query_branch_in_flight`) + 一个 ClassVar (`name`) + classmethod `probe()`。

**Supported backends (v1.31.0)**:

| Backend | name | Status | Real implementation? |
|---------|------|--------|---------------------|
| Aether | `aether-ci-cli` | ✅ Default, full | Yes (10CG Lab internal, migrated from v1.30.0 pre_merge_gate.py) |
| GitHub Actions | `github-actions` | 🚧 Stub | No — `probe()` real (`gh` CLI + auth check), `query_*()` raise `NotImplementedError`. Real implementation deferred to v1.32.0+ next cycle |

**Backend selection algorithm** (`resolve_ci_backend(config)`):

```
if config["ci_backends"] is [] (empty list):
    return None  # explicit disable per AC-4.5
elif config["ci_backends"] is non-empty list:
    try each entry in user-specified order, return first probe()=True
elif config["ci_backends"] is None or missing:
    iterate BACKENDS list (Aether → GHA), return first probe()=True
```

**BACKENDS list order** (`ci_backends/__init__.py` static import, Hard Constraint #8):

```python
BACKENDS: list[type[CIBackend]] = [AetherBackend, GitHubActionsBackend]
```

→ Aether-first precedence locked. **不允许** decorator-based registration / `setuptools.entry_points` / 任何 dynamic discovery。

**Config schema example**:

```jsonc
{
  "phase_c_integrator": {
    "pre_merge_gate": {
      "enabled": true,
      "ci_backends": null,                         // auto-detect (default)
      // OR: "ci_backends": [],                    // explicit disable
      // OR: "ci_backends": [{"name": "aether-ci-cli"}],  // explicit list
      "no_ci_fallback": "skip_with_warning",       // when no backend available
      // Legacy alias (auto-translated + DeprecationWarning, removed in v2.0):
      // "primitive_preference": ["aether-ci-cli"],
      // "no_aether_fallback": "skip_with_warning"
    }
  }
}
```

**Hard Constraint #7 (NIE-propagation safety)**:

Stub backend (e.g. `GitHubActionsBackend` in v1.31.0) `probe()` returns True 但 `query_*()` raise `NotImplementedError`。`gate_check()` **必须 propagate NIE to caller**,**不允许** catch-and-route-to-`no_ci_fallback`。理由:防止"装了 `gh` 但实际用 Aether 的项目"因 GHA stub 抢先注册而 Rule #8 静默降级。如需禁用 backend probing,显式设 `ci_backends: []`。

**Probe cache (Hard Constraint #11, Option B)**:

`ci_backends/__init__.py` exports `cached_probe(backend_cls)` + `reset_probe_cache()`。模块-level dict (`_probe_cache`) 缓存 probe 结果。**禁止** `@functools.lru_cache` (test isolation hazard)。测试 setUp/tearDown 必须调 `reset_probe_cache()` 防止状态泄漏。

**Adding a new backend** (e.g. GitLab CI):

1. Create `ci_backends/gitlab_ci.py` 继承 `CIBackend`,实现 `name` / `probe` / `query_pr_ci` / `query_branch_in_flight` (optionally `precheck`)
2. Update `ci_backends/__init__.py` 加 import + 加到 `BACKENDS` list (位置决定 precedence)
3. 加 unit tests 在 `tests/test_ci_backends.py`
4. 加 doc entry in this table

**NIE 是 stub 临时状态**: 如果新 backend 计划只做 stub,所有 `query_*()` 必须 raise `NotImplementedError` 带 operable message (含 `"PR welcome"` 提示 + 显式 disable instructions per `ci_backends: []`)。Hard Constraint #4 + AC-2.5 enforced via test。

---

### C.2.4.5 Submodule Pointer Regression Gate (v1.28.0+)

> **新增于 v1.28.0** — 实施 Spec [`aria-submodule-pointer-regression-gate`](../../../openspec/changes/aria-submodule-pointer-regression-gate/proposal.md) (Approved 2026-05-24, DEC-20260524-002)。
> 源于 2026-05-23 PR #123 silent submodule pointer regression incident (`6fea5d7` 静默回滚 4 commits, 被 post-merge audit catch + fast-forward fix `a8e0096`)。
> Closes Forgejo Aria [#124](https://forgejo.10cg.pub/10CG/Aria/issues/124)。

**Two-phase rollout** (✅ flipped to block default in v1.49.0, 2026-06-21):
- **v1.28.0** (history): `mode=warn` 默认 — 检测 + 日志 `WOULD-BLOCK`, 不阻止 merge
- **v1.49.0+** (current): `mode=block` 默认 — 检测到 regression/divergence + 无 override → 拒绝 merge
- Flip 依据: hard-date Trigger B + minimum-observation guard ≥3 gate executions (实测 5, all warn-PASS) + tripwire green (4 clean host-cron) + FP 0%; owner risk-accept sign-off 2026-06-21。决策记录见 `.aria/decisions/2026-06-21-v1.49.0-block-flip.md` (主仓)。

**触发条件**:
- §C.2.4 verdict=green (CI gate 已通过)
- 即将调用 branch-manager merge action
- 配置 `phase_c_integrator.submodule_gate.mode`: `"block"` (v1.49.0+ default) | `"warn"` (legacy opt-out) | `"off"` (skip 完全)

**执行流程** (Bash gate, 见 `scripts/submodule_gate.sh`):

```bash
# Step 1: fail-loud fetch with bounded retries (1s/2s/4s × 3 attempts)
# Drop fragile `grep success patterns` — use exit code only (AD-FOLLOWUP-1)
BEFORE_REMOTE=$(git rev-parse origin/master 2>/dev/null || echo "FIRST_RUN")

for delay in 1 2 4; do
    if git fetch origin 2>&1; then FETCH_OK=1; break; fi
    sleep "$delay"
done
[[ "${FETCH_OK:-0}" != 1 ]] && {
    log_telemetry "FETCH_FAILURE" "$BEFORE_REMOTE" "(unknown)" "fetch_exhausted_retries"
    echo "BLOCK: git fetch origin failed after 3 attempts" >&2
    exit 1
}

# Step 2: refspec assertion (skip on FIRST_RUN)
AFTER_REMOTE=$(git rev-parse origin/master)
if [[ "$BEFORE_REMOTE" != "FIRST_RUN" && "$BEFORE_REMOTE" != "$AFTER_REMOTE" ]]; then
    if ! git merge-base --is-ancestor "$BEFORE_REMOTE" "$AFTER_REMOTE" 2>/dev/null; then
        echo "BLOCK: origin/master rewritten (non-ancestor advance) — operator confirm required" >&2
        exit 1
    fi
fi

# Step 3: per-submodule loop
exit_code=0
while IFS= read -r SUB; do
    [[ -z "$SUB" ]] && continue
    git -C "$SUB" fetch origin 2>&1 >/dev/null || true

    FEATURE_PTR=$(git ls-tree HEAD "$SUB" | awk '{print $3}')
    MASTER_PTR=$(git ls-tree origin/master "$SUB" | awk '{print $3}')

    # nil-SHA: first-time submodule (master had no gitlink)
    [[ -z "$MASTER_PTR" ]] && {
        echo "INFO: $SUB first introduced this PR (no prior master gitlink); gate PASS"
        continue
    }
    # No-change
    [[ "$FEATURE_PTR" == "$MASTER_PTR" ]] && {
        echo "OK: $SUB unchanged"
        continue
    }

    echo "GATE: submodule=$SUB master=$MASTER_PTR feature=$FEATURE_PTR"

    # Forward bump?
    if git -C "$SUB" merge-base --is-ancestor "$MASTER_PTR" "$FEATURE_PTR" 2>/dev/null; then
        echo "PASS: $SUB forward bump"
        continue
    fi

    # Distinguish regression vs divergent
    if git -C "$SUB" merge-base --is-ancestor "$FEATURE_PTR" "$MASTER_PTR" 2>/dev/null; then
        VERDICT="REGRESSION"
    else
        VERDICT="DIVERGENT"
    fi

    # Check override (commit trailer OR PR label)
    if check_override "$SUB" "$MASTER_PTR" "$FEATURE_PTR"; then
        echo "ALLOW: $SUB $VERDICT overridden (audit logged)"
        log_override "$SUB" "$VERDICT" "$MASTER_PTR" "$FEATURE_PTR"
        continue
    fi

    # Mode dispatch
    MODE="${ARIA_SUBMODULE_GATE_MODE:-block}"
    if [[ "$MODE" == "warn" ]]; then
        echo "WOULD-BLOCK: submodule=$SUB master=$MASTER_PTR feature=$FEATURE_PTR reason=$VERDICT"
        log_warn "$SUB" "$VERDICT" "$MASTER_PTR" "$FEATURE_PTR"
    else
        echo "BLOCK: $VERDICT — submodule=$SUB master=$MASTER_PTR feature=$FEATURE_PTR" >&2
        echo "       Override 1: commit trailer 'Submodule-Rollback: $SUB $MASTER_PTR->$FEATURE_PTR reason=<reason>'" >&2
        echo "       Override 2: PR label 'submodule-rollback-approved'" >&2
        log_block "$SUB" "$VERDICT" "$MASTER_PTR" "$FEATURE_PTR"
        exit_code=1
    fi
done < <(git config --file .gitmodules --get-regexp '^submodule\..*\.path$' 2>/dev/null | awk '{print $2}')

exit $exit_code
```

**Override mechanism** (per-PR explicit, NOT sticky config):

1. **Commit trailer** in merge commit message (accepts both Unicode `→` and ASCII `->`):
   ```
   Submodule-Rollback: aria a8e0096→3b688a9 reason=v1.24.1 introduced critical regression
   Submodule-Rollback: aria a8e0096->3b688a9 reason=同 (ASCII alternative for LANG=C/POSIX safety)
   ```
   Parser resolves short SHAs (≥7 chars) via `git rev-parse` before comparison to FEATURE/MASTER pointers. Mismatched SHAs in trailer → reject override.

2. **PR label** `submodule-rollback-approved` (settable only by repo maintainers via Forgejo):
   - Fetched via `forgejo GET /repos/<owner>/<repo>/issues/<PR>/labels`
   - On API failure: treat as no-label (gate proceeds to mode dispatch)

3. Override audit log to `metrics/submodule-gate-overrides.jsonl` (JSONL append-only, race-safe):
   ```json
   {"timestamp":"...","pr_id":N,"submodule":"...","master_sha":"...","feature_sha":"...","verdict":"REGRESSION","reason":"...","override_type":"trailer"|"label"}
   ```

**Verdict 三态** (output):
- `pass` — all submodules: forward / no-change / first-time
- `warn` (legacy opt-out) — WOULD-BLOCK logged, merge proceeds
- `block` (v1.49.0+ default) — merge refused, exit 1
- `bypass` — override applied, audit logged, merge proceeds

**Output schema** (JSON):
```json
{
  "verdict": "pass" | "warn" | "block" | "bypass",
  "affected_submodules": [
    {
      "path": "aria",
      "master_sha": "a8e0096",
      "feature_sha": "3b688a9",
      "verdict": "REGRESSION",
      "override": "trailer" | "label" | null
    }
  ],
  "telemetry_files": {
    "warns": "metrics/submodule-gate-warns.jsonl",
    "overrides": "metrics/submodule-gate-overrides.jsonl",
    "blocks": "metrics/submodule-gate-blocks.jsonl",
    "misses": "metrics/submodule-gate-misses.jsonl"
  }
}
```

**配置参数**:
| 参数 | 默认 | 说明 |
|------|------|------|
| `mode` | `"block"` (v1.49.0+ default) / `"warn"` (legacy) / `"off"` | `"warn"` 仅日志 / `"block"` 拒绝 merge / `"off"` 跳过 gate |
| `fetch_retries` | `[1, 2, 4]` | 指数退避秒数 |
| `metrics_dir` | `"metrics/"` (relative to aria-plugin root, NOT main repo) | 4 JSONL append-only 文件位置 |
| `forgejo_api_timeout_s` | `5` | PR label fetch timeout |
| `nil_sha_action` | `"pass_with_info"` | first-time submodule (master 无 gitlink) → PASS + INFO log |

**降级行为**:
- `mode: off` → 完全跳过 §C.2.4.5 (与 v1.27.x 行为 100% 一致, 用于紧急 bypass 全 cycle)
- `git fetch origin` 3-attempt failure → terminal block (per `wait_recoverable` pattern, owner remediation: 检查 auth/network/URL drift)
- `git ls-tree` empty output (nil-SHA case) → PASS + INFO (first-time submodule case)
- `git merge-base --is-ancestor` exit 128 (SHA not in submodule DB after fetch) → BLOCK + "submodule fetch incomplete, retry" hint

**Performance budget**:
- Warm cache (local dev): ~310ms per submodule × 3 submodules ≈ ~930ms (well under §C.2.4 ~3-5s)
- CI cold-path: ~610-2110ms per submodule × 3 submodules ≈ ~1.8-6.3s (slightly higher than §C.2.4, ~comparable). Bounded retries on fetch failure add up to 7s only when fetch fails (excluded from steady-state budget).
- Phase B may add background-subshell parallel submodule loop if CI dogfood shows >5s sustained.

**Race condition 处理** (per backend-architect R3 missing scenario):
- Concurrent force-push to `origin/master` during gate execution: refspec assertion (Step 2) compares BEFORE/AFTER rev-parse
- Ancestry-forward change → continue (legitimate)
- Non-ancestor history rewrite → abort with operator confirm
- Deterministic pre-staged fixture in T-replay-9 validates detection logic (not true concurrency)

**Tripwire** (mechanical detection of (B+) gate misses):
- Weekly Forgejo Actions workflow at `.forgejo/workflows/submodule-gate-tripwire.yml` in `10CG/Aria` main repo (NOT `aria/cron/` in aria-plugin)
- Compares master HEAD~1 vs HEAD submodule gitlinks ancestry
- On regression escaped (B+) → append to `metrics/submodule-gate-misses.jsonl` + file Forgejo issue with label `gate-tripwire-count`
- Auto-promote (A) post-merge detector without re-brainstorm if any tripwire condition met:
  1. Regression escapes (B+) within 12 months OR 100 merges (whichever first)
  2. (B+) fetch-failure incident manifests
  3. Non-PR-flow regression (direct master push bypassing PR)
- v1.28.0 ships workflow as `on: workflow_dispatch` only; tripwire periodic execution migrated to **host-cron** (`0 4 * * 0`, v1.41.0 R-fix-2 — Actions runner 无 forgejo 凭据 + CF Access 墙; standalone `scripts/submodule-tripwire-audit.sh`). v1.49.0 block-flip 依赖 host-cron tripwire (4 clean runs) 作独立兜底, 非 workflow cron

**Helper 实现**: `${ARIA_PLUGIN_ROOT:-aria}/skills/phase-c-integrator/scripts/submodule_gate.sh` (Bash, stdlib + git only)

**Cross-references**:
- Spec: `openspec/changes/aria-submodule-pointer-regression-gate/proposal.md`
- DEC: `.aria/decisions/2026-05-24-aria-124-submodule-pointer-regression-gate.md`
- Convention doc: `standards/conventions/submodule-pointer-hygiene.md` (zero-code companion, NOT numbered Rule)
- Forgejo Aria #124
- Source incident: PR #123 merge `6fea5d7` + fast-forward fix `a8e0096`

---

### C.2.5 Multi-Remote Push Enforcement (v1.15.0+)

**触发条件**:
- Phase C.2 合并成功 (master 已 fast-forward)
- 配置 `phase_c_integrator.multi_remote_push.enabled: true` (默认)

**与 branch-manager 边界** (不重叠):
| Skill | 职责 | Remote 范围 |
|-------|------|-----------|
| branch-manager (C.2 PR 发起前) | 推送 feature 分支 + 创建 PR | 仅 origin |
| phase-c-integrator C.2.5 (PR 合并后) | 推送 master + 多 remote SHA 验证 | 所有 enforced remote |

**执行流程**:
1. 快照 `expected_sha = git rev-parse HEAD` (合并后本地 master HEAD)
2. 枚举子模块: `git submodule status --recursive`
3. 确定 `ENFORCED_REMOTES`: skill 级 `enforced_remotes == null` 时继承顶层 `multi_remote.enforced_remotes`, 空则自动发现所有 remote
4. **Per-Remote Matrix Gating** (对每个 REMOTE ∈ ENFORCED_REMOTES):
   - a. 遍历子模块, 调用 `git-remote-helper.push_all_remotes(SUBMODULE.path, SUBMODULE.branch, [REMOTE])`
   - b. 子模块推 REMOTE 任一失败 → 按失败优先级决策 (见下), 阻断则跳过本 REMOTE 的主仓库推送
   - c. 子模块全部成功 → 调用 helper.push_all_remotes(main_repo, branch, [REMOTE])
   - d. 主仓库推送成功 → 调用 helper.verify_parity_post_push(main_repo, branch, expected_sha, [REMOTE])
   - e. verify match=false → 同优先级决策
5. 所有 REMOTE 处理完毕, 全部通过 → 进入 Phase D
6. 任一阻断 → 输出具体失败 remote + 修复命令 (`git -C <path> push <remote> <branch>`)

**失败优先级** (决策表):
| 条件 | 行为 |
|------|------|
| remote ∈ `read_only_remotes` | warning 降级, 继续 (最高优先级) |
| `fail_on_partial_push: false` + 非 read_only | warning, 继续 |
| `fail_on_partial_push: true` + 非 read_only (默认) | **阻断**, 输出修复命令 |

**Per-Remote Matrix 示例**:
```
origin: sub1 ✅ sub2 ✅ main ✅ (已推)
github: sub1 ✅ sub2 ❌ (network timeout) → 跳过 main github; 但 origin 已完成
```

**子模块 detached HEAD**: 沿用 helper canonical (`detached_head: true` + HEAD SHA 比较), 警告但不阻断。

**降级策略**: 检测 `test -f "${ARIA_PLUGIN_ROOT:-aria}/skills/git-remote-helper/SKILL.md"` 存在性 (路径相对项目根; `ARIA_PLUGIN_ROOT` 环境变量优先)。不可用时用内联降级 (不重试, 简化实现), schema 仍一致。

**Race condition 处理**: verify 4 次 attempt 全部 match=false 默认阻断, 记录 "possible race condition"。

---

### C.2.6 UPM Milestone Sub-progress Append (v1.16.0+)

> **新增于 v1.16.0** — 修复 Forgejo #22 "multi-PR cycle UPM 信息盲区"问题。
> 源于 M1 closeout (2026-04-23) single-D.1 一次性更新 85 tasks 的实际痛点
> + silknode US-074 multi-PR migration 场景。

**触发条件**:
- C.2.5 已完成 (或已跳过)
- 配置 `upm.milestone_driven: true`
- commit message 或 spec change_id 中包含 `US-XXX` 模式

**关联识别逻辑**:

```bash
# 1. 从 commit message 中提取 US 编号
#    示例: "feat(m1): T4 complete — DEMO-001 E2E SUCCESS" 含 US-021 前缀
US_REF=$(git log -1 --format="%s %b" | grep -oE 'US-[0-9]+' | head -1)

# 2. 如果 commit message 无 US-XXX，尝试从 spec change_id 推断
if [ -z "$US_REF" ] && [ -n "$SPEC_CHANGE_ID" ]; then
  US_REF=$(grep -r "$SPEC_CHANGE_ID" openspec/changes/ \
    --include="proposal.md" -l | \
    xargs grep -oE 'US-[0-9]+' | head -1)
fi
```

**执行动作**:

```bash
# 获取当前 commit 信息
COMMIT_SHA=$(git rev-parse --short HEAD)
COMMIT_DATE=$(date +%Y-%m-%d)
COMMIT_TITLE=$(git log -1 --format="%s")
PR_URL="${PR_URL:-}"  # 来自 C.2 输出，无则留空

# 构造 sub-bullet
if [ -n "$PR_URL" ]; then
  SUB_BULLET="  - ${COMMIT_DATE}: ${COMMIT_SHA} — ${COMMIT_TITLE} (${PR_URL})"
else
  SUB_BULLET="  - ${COMMIT_DATE}: ${COMMIT_SHA} — ${COMMIT_TITLE}"
fi

# 定位 UPM 文档中对应 US 行并追加 sub-bullet
UPM_FILE=$(find . -name "unified-progress-management.md" \
  -not -path "*/archive/*" | head -1)

if [ -n "$UPM_FILE" ] && [ -n "$US_REF" ]; then
  # 将 [ ] IN_PROGRESS 更新为 [~] IN_PROGRESS (如当前状态为 [ ])
  sed -i "s/\[ \] \(.*${US_REF}.*\)/[~] \1/" "$UPM_FILE"
  # 在 US 行下方追加 sub-bullet
  sed -i "/.*${US_REF}.*/a\\${SUB_BULLET}" "$UPM_FILE"
fi
```

**状态标记约定**:

| 标记 | 含义 | 触发时机 |
|------|------|----------|
| `[ ]` | 未开始 / IN_PROGRESS (原有) | 初始状态 |
| `[~]` | 进行中，有中间进度记录 | C.2.6 首次追加时自动升级 |
| `[x]` | COMPLETED | D.1 final pass 写入 |

**sub-bullet 格式示例**:

```markdown
- [~] US-021: M1 MVP Layer 2 实现
  - 2026-04-20: abc1234 — feat(m1): T1 infra complete (https://forgejo.../pulls/18)
  - 2026-04-22: def5678 — feat(m1): T3 orchestrator ready (https://forgejo.../pulls/19)
  - 2026-04-23: ghi9012 — test(m1): T5 DEMO E2E complete (https://forgejo.../pulls/20)
```

**DoD**:
- `upm.milestone_driven=true` 时: UPM 文档对应 Story 行下出现 sub-bullet，状态从 `[ ]` 升级为 `[~]`
- `upm.milestone_driven=false` (默认) 时: Skill 无行为变化，完全向后兼容

**配置示例** (`.aria/config.json`):

```yaml
upm:
  milestone_driven: false  # 默认 false，保留 D.1-only 现有行为
                           # 设为 true 启用 C.2.6 中间进度追加
```

---

### 输出

```yaml
success: true
steps_executed: [C.1, C.2]
steps_skipped: []
results:
  C.1:
    commit_sha: "abc1234"
    commit_message: "feat(auth): 添加用户认证..."
  C.2:
    pr_url: "https://..."
    pr_number: 123

context_for_next:
  commit_sha: "abc1234"
  pr_url: "https://..."
```

---

## emergency hotfix: pre_merge → convergence (#58, v1.35.0)

当本 cycle 走 emergency_hotfix lane (state-scanner `emergency_hotfix` 规则触发, `hotfix/*` 分支) 时, C.2 的 pre_merge audit 调用点 **仅 `audit.enabled=true` 且 `audit.checkpoints.pre_merge != "off"` 时**, 把 audit mode 降级到 **convergence** (不 challenge) —— prod 紧急修复不必跑 15-30min challenge ceremony。

- advisory: phase-c-integrator 在 pre_merge 调 audit-engine 时传 emergency_hotfix lane 信号, audit-engine 据此 + file-scope 过滤共同 resolve 最终 mode (双降级幂等, 都 → convergence)。
- C.2.4 pre-merge precondition gate (CI passing 等) **不豁免** —— hotfix 仍须过 Rule #8 CI gate (紧急不等于跳 CI 验证)。
- 详见 [audit-engine SKILL.md](../audit-engine/SKILL.md) §emergency hotfix lane。

---

## Context 占用感知 (合并前长会话, #104)

C.2 合并前若会话已很长 (多 cycle 累积), 用 [aria-context-monitor](../aria-context-monitor/SKILL.md) 判断"本 cycle 收尾后是否该暂停换会话", 避免 pre-merge gate 等待期硬撞 context 上限丢上下文:

```bash
python3 "${CLAUDE_PLUGIN_ROOT:-aria}/skills/aria-token-telemetry/scripts/token_telemetry.py" --project-root .
```

`used_percentage` (relay 路径) `>85%` → 建议 merge 完成后即暂停 + 写 handoff。advisory, 不自动中断。详见 phase-b-developer 同名章节。

**会话收尾触发** (session-closer TASK-007): merge 完成后调 [closeout_trigger](../session-closer/scripts/closeout_trigger.py) —— 占用 ≥ 阈值 + 有未交接成果时 advise `/session-closer`(advisory, 不自动执行)。**契约 (I-1)**: 喂 `token_telemetry.py` 的输出 (含 `source`), 非 relay cache 原始文件 (先 `token_telemetry.py > $TEL` 再 `closeout_trigger.py --telemetry-json $TEL`, 详见 phase-b-developer 同名章节)。

---

## 跳过规则

| 条件 | 跳过步骤 | 检测方

…(truncated)
