# Commit-message 与 CLI 对齐设计

日期：2026-08-14  
状态：已确认（brainstorming）  
父规格：[2026-08-10-host-agent-design.md](./2026-08-10-host-agent-design.md)  
参考 CLI：`/Users/nietao/VSCode-plugins/smart-commit-cli` @ `peerReference.cliVersion`（当前 0.1.21）

## 1. 背景与目标

host-agent 的 `commitMessage.validation.protocol` / `pattern` 与 CLI 不一致：合法值是 `conventional | none | custom`，默认 `conventional`，且没有 `structure` / `skill` / `hybridGenerate`。两边无法按同一套规则更新。

本规格把 host-agent 的 **整个 `commitMessage.*` 子系统** 对齐到 CLI 当前语义。不保留旧写法，不做迁移别名，不做弃用期。

### 目标

1. `validation.protocol` 合法值与 CLI 相同：`none | conventional | semantic | gitmoji`。
2. `validation.pattern` 只做额外 subject 正则，不是协议。
3. 补齐 `structure`、`skill`、`hybridGenerate`、`maxDiffChars`。
4. 校验、prompt、provided/hybrid/generated 解析与 CLI 同语义（见非目标中的 repair 例外）。
5. 内置 git-commit-message skill 文件拷入本仓并注入 generate prompt。

### 非目标

- 不修改 `smart-commit-cli`。
- 不抽共享 npm 包。
- **不做 correction repair turn**：生成或 hybrid 第一次校验失败即 `COMMIT_MESSAGE_INVALID`。turn 协议保持单次 `purpose: "commit-message"`。
- 不做 chunked / hybrid **review**。
- 不新增 CLI 的 `--validation-protocol` / `SMART_COMMIT_*` 覆盖层。配置仍走 JSON 文件 + 已有 `--commit-message`。
- 不移植 `review.skill`。
- 不兼容旧值 `custom`，不把旧默认 `conventional` 留作未写 protocol 时的隐式行为。

## 2. 决策摘要

| 决策项 | 选择 |
|--------|------|
| 范围 | A+C：protocol/pattern + structure/skill/hybridGenerate |
| protocol 合法值 | `none \| conventional \| semantic \| gitmoji` |
| 默认 protocol | `none`（与 CLI 相同） |
| 旧值 `custom` | 解析失败，无别名 |
| pattern | 与 protocol 独立叠加；解析 trim；非法正则拒绝 |
| repair turn | 不做 |
| 内置 skill | 拷贝 CLI 的 `git-commit-message-skills` 并注入 prompt |
| 实现方式 | 在现有 `src/commitMessage/*` 与 `src/config/*` 外科移植，不整文件替换、不抽包 |
| CLI flag/env 覆盖 | 不加 |

## 3. 配置契约

`HostAgentConfig.commitMessage` 对齐 CLI 的非 LLM 字段：

```ts
commitMessage: {
  input: string;
  language: OutputLanguage;
  maxDiffChars: number;
  structure: "subjectOnly" | "subjectBody" | "subjectBodyFooter";
  autoGenerate: boolean;
  hybridGenerate: boolean;
  skill: {
    id: string;
    path: string;
    promptTuning: string;
  };
  validation: {
    protocol: "none" | "conventional" | "semantic" | "gitmoji";
    pattern: string;
    extractTicketIdFromBranch: boolean;
    requireTicketIdInMessage: boolean;
  };
}
```

### 3.1 默认值

与 CLI `defaultCliConfig.commitMessage` 对齐：

| 字段 | 默认 |
|------|------|
| `input` | `""` |
| `language` | `zh-cn` |
| `maxDiffChars` | `150000` |
| `structure` | `subjectOnly` |
| `autoGenerate` | `true` |
| `hybridGenerate` | `false` |
| `skill.id` | `conventional` |
| `skill.path` | `""` |
| `skill.promptTuning` | `""` |
| `validation.protocol` | `none` |
| `validation.pattern` | `""` |
| `validation.extractTicketIdFromBranch` | `true` |
| `validation.requireTicketIdInMessage` | `false` |

含义：默认 **skill 引导 Conventional 风格**，但 **不强制 protocol 校验**。与当前 host-agent「默认就按 Conventional 卡住」不同，这是有意的行为变化。

### 3.2 解析

`parseCommitMessageValidationProtocol` 对齐 CLI：

1. `trim().toLowerCase()`。
2. 空字符串或 `"none"` → `"none"`。
3. 否则必须是 `conventional` / `semantic` / `gitmoji`。
4. 其他值（含 `custom`）报错：`must be one of: none, conventional, semantic, gitmoji.`

`parseCommitMessageStructure`：精确匹配 `subjectOnly` / `subjectBody` / `subjectBodyFooter`。

`validation.pattern`：`parseString` 后 `trim()`。`validateHostAgentConfig` 调用 `assertValidRegexPattern`：空 pattern 通过；非空则 `new RegExp`，失败则配置错误。

`commitMessage.language` 用 `parseOutputLanguage`，不再当任意字符串。

`skill`：

- `path` 非空：不校验 `id` 是否属于内置集合（自定义 skill 文件优先，与 CLI 相同）。
- `path` 为空：`id` 必须是 `conventional` / `semantic` / `gitmoji`。空 id 非法。

`maxDiffChars`：合并后必须 `>= 1000`。

嵌套 merge：`commitMessage.skill` 与 `commitMessage.validation` 做一层浅合并，避免覆盖丢失未写字段。

### 3.3 截断

`commit-message generate` 与 `bridge` 的 commit-message turn 使用 `commitMessage.maxDiffChars`，不再借用 `review.maxDiffChars`。审查 turn 仍用 `review.maxDiffChars`。

## 4. 校验

`validateAndFinalizeCommitMessage` 对齐 CLI `src/commitMessage/protocol.ts`，适配 `HostAgentConfig`。

顺序：

1. 按 `structure` 做结构校验。空内容 → `COMMIT_MESSAGE_REQUIRED`。含 markdown 代码围栏、结构不合法 → `COMMIT_MESSAGE_INVALID`。
2. 按 structure 规范化（`subjectOnly` 只保留 subject 行；其他保留 subject/body/footer）。
3. `extractTicketIdFromBranch` 为真时，若 subject 没有 ticket-like ID，则从分支名注入。typed subject（`type(scope)?: summary`）插入 summary；gitmoji subject（`emoji + summary`）插在 emoji 之后；否则前置到整行。body/footer 保留。
4. `protocol !== "none"` 时校验 subject：
   - `gitmoji`：`/^\S+\s+(.+)$/u`，summary 非空，再对去掉 leading ticket 后的 summary 做语言校验。
   - `semantic`：`type(scope)?: summary`（`type` 为 `[a-z]+`）；不限制 Conventional type 白名单；summary 语言校验。
   - `conventional`：同上格式，且 type 必须属于 `feat|fix|refactor|perf|docs|test|style|build|ci|chore|revert`；summary 语言校验。
5. `requireTicketIdInMessage` 为真且 subject 无 ticket-like ID → 失败。
6. `pattern` 非空且 subject 不匹配 → 失败。

语言校验复用 CLI `validateLanguageText`（script / english / latin + 禁止非目标文字）。`protocol === "none"` 时不做语言校验。

错误码不变：`COMMIT_MESSAGE_REQUIRED` / `COMMIT_MESSAGE_INVALID`。

## 5. Prompt 与 skill 注入

### 5.1 生成 messages

从 CLI `buildCommitMessageMessages` 移植，输入增加 `structure` 与可选 `userDraft`。

- system：按 structure 选择 intro；protocol 为 none 时声明不强制协议，否则要求遵循对应协议名。
- user：仓库、分支、language、structure、protocol、structure/protocol 输出规则、changed files、可选 User draft、staged diff。
- `promptAugmentation` 追加到 system（skill 正文）。

`COMMIT_MESSAGE_RESPONSE_SCHEMA`（turn 契约）随 structure 变化，不再写死「单行 Conventional Commits」：

- `subjectOnly`：单行 subject，纯文本。
- `subjectBody`：subject + 可选 body，纯文本。
- `subjectBodyFooter`：subject + 可选 body + 可选 footer，纯文本。

不实现 `buildCommitMessageRepairMessages`（无 repair turn）。

### 5.2 Skill 注入

新增 `src/promptContext.ts`，从 CLI 移植 `buildPromptAugmentation` 的 git-commit-message 部分：

1. `skill.path` 非空：相对 `repositoryPath`（或绝对路径）读文件；空文件报错。
2. 否则按 `skill.id` 加载内置 bundle：`SKILL.md`（去 frontmatter）+ `references/*.md`。
3. `promptTuning` 非空则再追加 tuning 段。

内置目录从 CLI 拷贝：

```
src/git-commit-message-skills/conventional/SKILL.md
src/git-commit-message-skills/conventional/references/examples.md
src/git-commit-message-skills/semantic/SKILL.md
src/git-commit-message-skills/semantic/references/examples.md
src/git-commit-message-skills/gitmoji/SKILL.md
src/git-commit-message-skills/gitmoji/references/examples.md
```

运行时查找路径对齐 CLI：`__dirname` 下、`../src/git-commit-message-skills/<id>`、`cwd/src/git-commit-message-skills/<id>`。

`package.json` `files` 增加 `src/git-commit-message-skills`（tsc 不复制 markdown）。

## 6. 解析流程

`resolveHostAgentCommitMessage`：

1. `initial = (providedInput ?? config.commitMessage.input).trim()`。
2. `initial` 非空且 `hybridGenerate === false`：本地校验，`source: "provided"`，不发 turn。
3. `initial` 非空且 `hybridGenerate === true`：一次 `complete` turn（messages 含 userDraft），校验成功则 `source: "hybrid"`；校验失败则 `COMMIT_MESSAGE_INVALID`。
4. `initial` 为空且 `autoGenerate === true`：一次 `complete` turn，`source: "generated"`；失败同上。
5. 否则：`COMMIT_MESSAGE_REQUIRED`。

`CommitMessageResolutionResult.source` 变为 `"provided" | "generated" | "hybrid"`。

`commit-message generate` 的 `status` 枚举不新增 `hybrid`：hybrid 成功时 `status` 为 `"generated"`，`commitMessageSource` 为 `"hybrid"`。bridge / passHistory 已有 `hybrid` 源，接上即可。

Turn：`kind: "complete"`，`purpose: "commit-message"`，`attempt: 0`。失败不写第二次 request。

## 7. 文档与示例

更新：

- `docs/configuration.md`：`commitMessage.*` 表、合法 protocol、structure、hybrid 行为、skill。
- `docs/parity-matrix.md`：commit-message / bridge 备注改为已对齐 structure/skill/hybrid（仍注明无 correction repair）。
- `docs/contracts.md`：如有 protocol 枚举则改。
- `README.md` 示例若写了 `protocol: "conventional"` 作为默认，改为与新默认一致（省略或 `none`）。需要强制 Conventional 的示例显式写 `protocol` + `skill.id`。

不写迁移指南，不保留 `custom` 文档。

## 8. 测试

至少覆盖：

- 解析：`none` / `conventional` / `semantic` / `gitmoji`；大小写与空白；`custom` 与未知值失败；空 protocol → `none`。
- pattern：trim；非法正则配置失败；合法 pattern 与 protocol 叠加。
- 默认：未写 protocol 时为 `none`；`feat: x` 与非 conventional 文本在默认下都可通过（无 protocol 校验）。
- conventional：未知 type 失败；合法 type 通过；`zh-cn` 时 summary 缺中文失败。
- semantic：`type` 为小写字母时 `type: summary` 通过（不必在 Conventional 白名单）；格式不对失败。
- gitmoji：`language: en` 时 `✨ add login` 一类通过；无 emoji/summary 失败。`language: zh-cn` 时英文-only summary 失败。
- structure：`subjectOnly` 拒绝多行；`subjectBody` 保留 body；ticket 注入不丢掉 body。
- hybrid：有 input + `hybridGenerate` → 发 turn 且 source 为 `hybrid`；prompt 含 User draft。
- provided：有 input + `hybridGenerate=false` → 不发 turn。
- skill：默认 prompt 含 bundled conventional 指令；`skill.path` 注入自定义文件。
- 现有 `commitMessageGenerate` / `bridgeFull` 测试随默认 protocol 变化而更新，不再假设默认 Conventional 校验。

## 9. 破坏性变化（有意，无兼容层）

1. 默认 `protocol` 从 `conventional` 改为 `none`。未写 protocol 的配置不再拒绝非 Conventional subject。
2. `custom` 不再合法。原 `protocol: "custom"` + `pattern` 的配置改为 `protocol: "none"` 并保留 `pattern`。
3. `commitMessage.language` 必须是支持的 `OutputLanguage`。
4. commit-message turn 的 diff 截断改用 `commitMessage.maxDiffChars`（默认 150000，低于当前借用的 `review.maxDiffChars` 200000）。
)
