kld-sdd 2.4.14 → 2.4.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # kld-sdd
2
2
 
3
- KLD SDD OpenSpec 项目初始化工具 - 一键初始化 AI 编辑器命令与技能,支持完整的 SDD(规格驱动开发)研发工作流。
3
+ KLD SDD OpenSpec 项目初始化工具 - 一键初始化 AI 编辑器技能,支持完整的 SDD(规格驱动开发)研发工作流。
4
4
 
5
5
  ## 这是什么?
6
6
 
7
7
  **SDD(Specification-Driven Development)** 是一种以文档链驱动 AI 编码的研发方法:先写清楚"要做什么",再让 AI 去实现,避免 AI 乱猜、反复返工。
8
8
 
9
- `kld-sdd` 帮你在项目中一键配置好这套工作流所需的全部 AI 命令和技能,支持 **Cursor、Claude Code、CodeBuddy、Qoder、OpenCode、KunlunZhima、WorkBuddy、Codex** 等编辑器。
9
+ `kld-sdd` 帮你在项目中一键配置好这套工作流所需的全部 AI 技能,支持 **Cursor、Claude Code、CodeBuddy、Qoder、OpenCode、KunlunZhima、WorkBuddy、Codex** 等编辑器。
10
10
 
11
11
  ## 快速开始
12
12
 
@@ -15,21 +15,21 @@ KLD SDD OpenSpec 项目初始化工具 - 一键初始化 AI 编辑器命令与
15
15
  npx kld-sdd
16
16
  ```
17
17
 
18
- 初始化完成后,打开你的 AI 编辑器,就能用 `/opsx:*` 命令开始工作了。
18
+ 初始化完成后,打开你的 AI 编辑器,激活 `opsx-*` skills 开始工作。
19
19
 
20
20
  ---
21
21
 
22
- ## 核心工作流:5 个命令
22
+ ## 核心工作流:5 个 skills
23
23
 
24
- 初始化后,你会得到以下 6 个 `/opsx:*` 命令,按顺序使用:
24
+ 初始化后,你会得到以下 SDD skills,按顺序使用:
25
25
 
26
26
  ```
27
- propose → spec → design → task → check
28
-
29
- skill(独立使用)
27
+ opsx-propose → opsx-spec → opsx-design → opsx-task → opsx-check
28
+
29
+ opsx-knowledge(独立使用)
30
30
  ```
31
31
 
32
- ### 第一步:`/opsx:propose <变更名称>`
32
+ ### 第一步:`opsx-propose <变更名称>`
33
33
 
34
34
  **做什么**:创建业务意图文档,明确"为什么要做这件事"。
35
35
 
@@ -44,7 +44,7 @@ propose → spec → design → task → check
44
44
 
45
45
  ---
46
46
 
47
- ### 第二步:`/opsx:spec <变更名称>`
47
+ ### 第二步:`opsx-spec <变更名称>`
48
48
 
49
49
  **做什么**:创建技术契约文档,明确"要实现什么 API 和规范"。
50
50
 
@@ -64,7 +64,7 @@ propose → spec → design → task → check
64
64
 
65
65
  ---
66
66
 
67
- ### 第三步:`/opsx:design <变更名称>`
67
+ ### 第三步:`opsx-design <变更名称>`
68
68
 
69
69
  **做什么**:创建技术实现方案,明确"怎么做"(聚焦本业务,不写全局架构)。
70
70
 
@@ -82,7 +82,7 @@ propose → spec → design → task → check
82
82
 
83
83
  ---
84
84
 
85
- ### 第四步:`/opsx:task <变更名称>`
85
+ ### 第四步:`opsx-task <变更名称>`
86
86
 
87
87
  **做什么**:将 design.md 拆解为 AI 可执行的原子任务列表。
88
88
 
@@ -102,11 +102,11 @@ propose → spec → design → task → check
102
102
 
103
103
  ---
104
104
 
105
- ### 第五步:`/opsx:check <变更名称>`
105
+ ### 第五步:`opsx-check <变更名称>`
106
106
 
107
107
  **做什么**:质量门禁检查,验证文档链的完整性、一致性和可执行性。
108
108
 
109
- **在让 AI 写代码之前,先运行这个命令。**
109
+ **在让 AI 写代码之前,先激活这个 skill。**
110
110
 
111
111
  **检查内容**:
112
112
  | 检查项 | 说明 |
@@ -119,20 +119,9 @@ propose → spec → design → task → check
119
119
 
120
120
  ---
121
121
 
122
- ### 独立使用:`/opsx:skill <文档路径>`
122
+ ### 独立使用:`opsx-knowledge`
123
123
 
124
- **做什么**:把你的团队规范文档(代码规范、API 设计规范等)转化为标准的 **Claude Skills 格式**技能文件,让 AI 每次都遵守你的团队规范。
125
-
126
- **适合场景**:
127
- - 把公司 Java 编码规范文档 → 生成 `java-code-style` 技能
128
- - 把 API 设计规范文档 → 生成 `api-design-rules` 技能
129
- - 把架构规范文档 → 生成 `arch-guidelines` 技能
130
-
131
- **AI 会做什么**:
132
- 1. 读取你的规范文档,提炼四类规则(必须/禁止/推荐/例外)
133
- 2. 识别规则模糊或冲突时,向你澄清
134
- 3. 生成带 YAML frontmatter 的标准 SKILL.md
135
- 4. 你确认输出路径后写入
124
+ **做什么**:查询 MM/CO 业务知识库,辅助理解业务名词和领域规则。
136
125
 
137
126
  ---
138
127
 
@@ -192,16 +181,12 @@ your-project/
192
181
  │ ├── design.md
193
182
  │ └── task.md
194
183
  ├── .cursor/
195
- │ ├── commands/opsx/ # opsx 命令(9个)
196
184
  │ └── skills/opsx-*/ # SDD skills(扁平一层)
197
185
  ├── .claude/
198
- │ ├── commands/opsx/ # opsx 命令(9个)
199
186
  │ └── skills/opsx-*/ # SDD skills(扁平一层)
200
187
  ├── .codebuddy/
201
- │ ├── commands/opsx/ # opsx 命令(9个)
202
188
  │ └── skills/opsx-*/ # SDD skills(扁平一层)
203
189
  └── .agents/
204
- ├── commands/opsx/ # Codex opsx 命令(9个)
205
190
  └── skills/opsx-*/ # Codex 项目级 skills
206
191
  ```
207
192
 
@@ -209,18 +194,18 @@ your-project/
209
194
 
210
195
  ## 支持的 AI 编辑器
211
196
 
212
- | 编辑器 | 命令格式 | 技能支持 | 自动创建目录 |
197
+ | 编辑器 | 技能格式 | 技能支持 | 自动创建目录 |
213
198
  |-------|---------|---------|------------|
214
- | Cursor | `.md` | ✅ | 已存在时 |
215
- | Claude Code | `.md` | ✅ | 已存在时 |
216
- | CodeBuddy | `.md` | ✅ | 自动创建 |
217
- | Qoder | `.md` | ✅ | 自动创建 |
218
- | OpenCode | `.md` | ✅ | 自动创建 |
219
- | KunlunZhima | `.md` | ✅ | 自动创建 |
220
- | WorkBuddy | `.md` | ✅ | 自动创建 |
221
- | Codex | `.md`(`.agents/commands/opsx/`) | ✅(`.agents/skills/`) | 自动创建 |
199
+ | Cursor | `SKILL.md` | ✅ | 已存在时 |
200
+ | Claude Code | `SKILL.md` | ✅ | 已存在时 |
201
+ | CodeBuddy | `SKILL.md` | ✅ | 自动创建 |
202
+ | Qoder | `SKILL.md` | ✅ | 自动创建 |
203
+ | OpenCode | `SKILL.md` | ✅ | 自动创建 |
204
+ | KunlunZhima | `SKILL.md` | ✅ | 自动创建 |
205
+ | WorkBuddy | `SKILL.md` | ✅ | 自动创建 |
206
+ | Codex | `SKILL.md`(`.agents/skills/`) | | 自动创建 |
222
207
 
223
- > Codex 初始化会创建 `.agents/commands/opsx/` 和 `.agents/skills/opsx-*/`,用于项目级命令与技能。
208
+ > Codex 初始化会创建 `.agents/skills/opsx-*/`,用于项目级技能。
224
209
 
225
210
  ---
226
211
 
@@ -230,7 +215,7 @@ your-project/
230
215
 
231
216
  1. 在项目中运行 `kld-sdd-init` 一次
232
217
  2. 将团队规范文档放入 `team-configs/` 目录
233
- 3. `/opsx:skill` 把规范转为技能,放入 `team-configs/skills/`
218
+ 3. 将自定义 skills 放入 `.*/skills/` 目录
234
219
  4. 提交到 Git
235
220
 
236
221
  ### 新成员 onboarding
@@ -241,7 +226,7 @@ npm install
241
226
  npx kld-sdd # 一键配置好所有 AI 工具
242
227
  ```
243
228
 
244
- 之后直接在编辑器中使用 `/opsx:propose` 开始工作。
229
+ 之后直接在编辑器中激活 `opsx-propose` 开始工作。
245
230
 
246
231
  ---
247
232
 
@@ -269,19 +254,19 @@ npx kld-sdd # 一键配置好所有 AI 工具
269
254
 
270
255
  **Q:openspec 是什么,必须安装吗?**
271
256
 
272
- A:`openspec` 是管理 SDD 变更目录的 CLI 工具(负责创建变更目录和读取指导规则)。初始化时会自动安装,如果安装失败也可以跳过(`--skip-openspec`),手动创建 `openspec/changes/<name>/` 目录后命令仍然可用。
257
+ A:`openspec` 是管理 SDD 变更目录的 CLI 工具(负责创建变更目录和读取指导规则)。初始化时会自动安装,如果安装失败也可以跳过(`--skip-openspec`),手动创建 `openspec/changes/<name>/` 目录后 skills 仍然可用。
273
258
 
274
259
  **Q:我用 CodeBuddy 或 Codex,目录不存在怎么办?**
275
260
 
276
- A:不用担心,`kld-sdd-init` 会自动创建 `.codebuddy/commands/opsx/`、`.codebuddy/skills/`、`.agents/commands/opsx/` 和 `.agents/skills/` 目录。
261
+ A:不用担心,`kld-sdd-init` 会自动创建 `.codebuddy/skills/`、`.agents/skills/` 等目录。
277
262
 
278
- **Q:`/opsx:propose` 和直接让 AI 写代码有什么区别?**
263
+ **Q:`opsx-propose` 和直接让 AI 写代码有什么区别?**
279
264
 
280
265
  A:直接让 AI 写代码,AI 会凭空假设很多细节,导致返工。SDD 流程强制先把"要做什么"写清楚,AI 按契约实现,减少歧义和返工。
281
266
 
282
267
  **Q:团队规范已经有了,怎么让 AI 每次都遵守?**
283
268
 
284
- A:用 `/opsx:skill <规范文档路径>` 把规范文档转成技能文件,部署到 `.*/skills/` 目录后,AI 编辑器激活该技能时会自动遵守这些规范。
269
+ A:将规范文档转为 skill 文件,部署到 `.*/skills/` 目录后,AI 编辑器激活该 skill 时会自动遵守这些规范。
285
270
 
286
271
  ---
287
272
 
package/lib/init.js CHANGED
@@ -4,10 +4,9 @@
4
4
  * 功能:
5
5
  * 1. 检测并自动安装 openspec
6
6
  * 2. 执行 openspec init 生成基础结构
7
- * 3. 覆盖 opsx 命令(9个独立命令)
8
- * 4. 部署 opsx skills
9
- * 5. 复制标准文档模版到项目
10
- * 6. 更新 .gitignore
7
+ * 3. 部署 opsx skills
8
+ * 4. 复制标准文档模版到项目
9
+ * 5. 更新 .gitignore
11
10
  */
12
11
 
13
12
  const fs = require('fs');
@@ -162,7 +161,7 @@ async function selectEditor() {
162
161
  console.log(' 6. KunlunZhima');
163
162
  console.log(' 7. WorkBuddy');
164
163
  console.log(' 8. Codex');
165
- console.log(' 9. 全部(为所有编辑器生成命令)');
164
+ console.log(' 9. 全部(为所有编辑器生成 skills)');
166
165
 
167
166
  const answer = await ask('请输入选项 (1-9): ');
168
167
  const selected = mapping[answer];
@@ -445,64 +444,6 @@ function cleanupNativeOpenspecCommands(selectedTools = Object.keys(TOOL_CONFIGS)
445
444
  return true;
446
445
  }
447
446
 
448
- /**
449
- * 覆盖 opsx 命令(9 个 SDD 工作流命令)
450
- * 所有编辑器统一使用 templates/opsx-commands/ 下的 SDD 命令模板
451
- * @param {string[]} selectedTools - 用户选择的编辑器列表
452
- */
453
- function overrideOpsxCommands(selectedTools = Object.keys(TOOL_CONFIGS)) {
454
- console.log('🔧 正在覆盖 opsx 命令...');
455
-
456
- const pkgPath = getPackagePath();
457
- const cwd = process.cwd();
458
-
459
- // 统一使用 opsx-commands 模板(9 个 SDD 命令,log 为内部自动触发)
460
- const sddCommands = ['propose.md', 'spec.md', 'design.md', 'task.md', 'check.md', 'apply.md', 'test.md', 'archive.md', 'explore.md'];
461
- const templatePath = path.join(pkgPath, 'templates', 'opsx-commands');
462
-
463
- if (!fs.existsSync(templatePath)) {
464
- console.log(`⚠️ SDD 命令模板目录不存在: ${templatePath}`);
465
- return false;
466
- }
467
-
468
- // 为用户选择的每个编辑器部署命令
469
- for (const toolKey of selectedTools) {
470
- const config = TOOL_CONFIGS[toolKey];
471
- if (!config) continue;
472
-
473
- const configDir = path.join(cwd, config.configDir);
474
- const targetOpsxDir = path.join(cwd, config.opsxDir);
475
-
476
- // 自动创建配置目录
477
- if (!fs.existsSync(configDir)) {
478
- console.log(` 自动创建 ${config.name} 配置目录...`);
479
- fs.mkdirSync(configDir, { recursive: true });
480
- }
481
-
482
- // 确保 opsx 命令目录存在
483
- if (!fs.existsSync(targetOpsxDir)) {
484
- fs.mkdirSync(targetOpsxDir, { recursive: true });
485
- }
486
-
487
- console.log(` 覆盖 ${config.name} 的 opsx 命令(9 个 SDD 命令)...`);
488
-
489
- // 复制所有 SDD 命令到 opsx 目录
490
- for (const cmd of sddCommands) {
491
- const sourceFile = path.join(templatePath, cmd);
492
- const targetFile = path.join(targetOpsxDir, cmd);
493
-
494
- if (fs.existsSync(sourceFile)) {
495
- const rendered = renderTemplate(fs.readFileSync(sourceFile, 'utf8'), config, toolKey);
496
- fs.writeFileSync(targetFile, rendered, 'utf8');
497
- console.log(` ✓ ${cmd}`);
498
- }
499
- }
500
- }
501
-
502
- console.log('✅ opsx 命令覆盖完成');
503
- return true;
504
- }
505
-
506
447
  /**
507
448
  * 清理旧版嵌套 bundle(skills/kld-sdd/opsx-*)
508
449
  * Claude Code 只识别 skills/<skill-name>/SKILL.md 一层,嵌套 bundle 无法被 / 菜单发现
@@ -606,8 +547,8 @@ function hookCommandText(hook) {
606
547
 
607
548
  function isSddClaudeHook(hook) {
608
549
  const text = hookCommandText(hook);
609
- return /(?:^|\s|["'])\.claude[\\/]+hooks[\\/]+sdd-(?:prompt|pre-tool|post-tool|stop)\.(?:cjs|js)(?=$|\s|["'])/i.test(text) ||
610
- /(?:^|\s|["'])\$\{CLAUDE_PROJECT_DIR\}[\\/]+\.claude[\\/]+hooks[\\/]+sdd-(?:prompt|pre-tool|post-tool|stop)\.(?:cjs|js)(?=$|\s|["'])/i.test(text);
550
+ return /(?:^|\s|["'])\.claude[\\/]+hooks[\\/]+sdd-(?:prompt|pre-tool|post-tool|stop|apply-gate)\.(?:cjs|js)(?=$|\s|["'])/i.test(text) ||
551
+ /(?:^|\s|["'])\$\{CLAUDE_PROJECT_DIR\}[\\/]+\.claude[\\/]+hooks[\\/]+sdd-(?:prompt|pre-tool|post-tool|stop|apply-gate)\.(?:cjs|js)(?=$|\s|["'])/i.test(text);
611
552
  }
612
553
 
613
554
  function sameHookCommand(left, right) {
@@ -689,7 +630,7 @@ function deployClaudeHookPack(selectedTools = Object.keys(TOOL_CONFIGS)) {
689
630
 
690
631
  fs.writeFileSync(targetSettingsPath, JSON.stringify(existingSettings, null, 2) + '\n', 'utf8');
691
632
  console.log(' ✓ 合并 .claude/settings.json hooks 配置');
692
- console.log('✅ Claude Code SDD Hook Pack 已就绪(可选增强,核心采集仍由 OPSX 模板完成)');
633
+ console.log('✅ Claude Code SDD Hook Pack 已就绪(可选增强,核心采集仍由 OPSX skills 完成)');
693
634
  return true;
694
635
  }
695
636
 
@@ -816,7 +757,7 @@ function initGlobalOverview() {
816
757
 
817
758
  console.log(`✅ 全局架构约束已初始化: ${targetFile}`);
818
759
  console.log(' ℹ️ 此文件定义了全局数据字典、接口规范、共享实体等约束');
819
- console.log(' ℹ️ 执行 /opsx:design 或 /opsx:task 时会自动读取此文件');
760
+ console.log(' ℹ️ 执行 opsx-design 或 opsx-task 时会自动读取此文件');
820
761
 
821
762
  return true;
822
763
  }
@@ -1027,7 +968,7 @@ KLD SDD 项目初始化工具
1027
968
 
1028
969
  示例:
1029
970
  kld-sdd-init # 完整初始化流程
1030
- kld-sdd-init --skip-openspec # 跳过 openspec,直接部署命令
971
+ kld-sdd-init --skip-openspec # 跳过 openspec,直接部署 skills
1031
972
  kld-sdd-init --tool codex # 仅部署 Codex 配置
1032
973
  `);
1033
974
  }
@@ -1080,19 +1021,16 @@ async function main() {
1080
1021
  // 2. 清理原生 openspec 命令(删除 opsx-*.md,避免命名混淆)
1081
1022
  cleanupNativeOpenspecCommands(selectedTools);
1082
1023
 
1083
- // 3. 部署 SDD opsx 命令(根据编辑器类型使用不同模板)
1084
- overrideOpsxCommands(selectedTools);
1085
-
1086
- // 4. 清理旧版嵌套 skills/kld-sdd/ bundle(Claude Code 无法识别二层目录)
1024
+ // 3. 清理旧版嵌套 skills/kld-sdd/ bundle(Claude Code 无法识别二层目录)
1087
1025
  cleanupLegacyBundledOpsxSkills(selectedTools);
1088
1026
 
1089
- // 4.1 部署 SDD opsx skills(扁平到 .claude/skills/opsx-*/)
1027
+ // 3.1 部署 SDD opsx skills(扁平到 .claude/skills/opsx-*/)
1090
1028
  deployOpsxSkills(selectedTools);
1091
1029
 
1092
- // 4.2 清理原生 openspec-* skills(防止残留)
1030
+ // 3.2 清理原生 openspec-* skills(防止残留)
1093
1031
  cleanupNativeOpenspecSkills(selectedTools);
1094
1032
 
1095
- // 4.2 部署 Claude Code Hook Pack(可选增强)
1033
+ // 3.3 部署 Claude Code Hook Pack(可选增强)
1096
1034
  deployClaudeHookPack(selectedTools);
1097
1035
 
1098
1036
  // 5. 部署 SDD Telemetry 数据目录
@@ -1123,7 +1061,7 @@ async function main() {
1123
1061
  console.log('已生成/覆盖:');
1124
1062
  console.log(' 🌐 openspec/specs/overview.md # 全局架构约束(数据字典、接口规范)');
1125
1063
  console.log(' 📄 openspec-templates/ # 标准文档模版(参考用)');
1126
- console.log(' 🔧 .*/commands/opsx/ # 9 个 SDD 命令');
1064
+ console.log(' 🎯 .*/skills/opsx-*/ # SDD skills(扁平一层)');
1127
1065
  if (selectedTools.includes('claude')) {
1128
1066
  console.log(' 🪝 .claude/hooks/ # Claude Code SDD Hook Pack(可选增强)');
1129
1067
  }
@@ -1132,26 +1070,27 @@ async function main() {
1132
1070
  console.log(' 🧪 skywalk-sdd/ci/ # CI 兜底采集模板');
1133
1071
  console.log();
1134
1072
 
1135
- // 统一的命令格式说明
1136
- console.log('可用命令(/opsx:* 系列,9 个 SDD 命令):');;
1137
- console.log(' /opsx:propose - 创建业务意图文档(Why)');
1138
- console.log(' /opsx:spec - 创建技术契约文档(What)');
1139
- console.log(' /opsx:design - 创建技术实现方案(How)');
1140
- console.log(' /opsx:task - 拆解AI编码任务(Do)');
1141
- console.log(' /opsx:check - 质量门禁检查(Verify)');
1142
- console.log(' /opsx:apply - 申请变更实施(Apply)');
1143
- console.log(' /opsx:test - 执行单元测试(Test)');
1144
- console.log(' /opsx:archive - 归档变更(Archive)');
1145
- console.log(' /opsx:explore - 浏览变更状态(Explore)');
1073
+ // 统一的 skill 格式说明
1074
+ console.log('可用 skills(opsx-* 系列,10 个 SDD skills):');
1075
+ console.log(' opsx-propose - 创建业务意图文档(Why)');
1076
+ console.log(' opsx-spec - 创建技术契约文档(What)');
1077
+ console.log(' opsx-design - 创建技术实现方案(How)');
1078
+ console.log(' opsx-task - 拆解AI编码任务(Do)');
1079
+ console.log(' opsx-check - 质量门禁检查(Verify)');
1080
+ console.log(' opsx-apply - 申请变更实施(Apply)');
1081
+ console.log(' opsx-test - 执行单元测试(Test)');
1082
+ console.log(' opsx-archive - 归档变更(Archive)');
1083
+ console.log(' opsx-explore - 浏览变更状态(Explore)');
1084
+ console.log(' opsx-knowledge - 业务知识库检索(辅助)');
1146
1085
 
1147
1086
  console.log();
1148
1087
  console.log('后续步骤:');
1149
- console.log(' 1. 运行 /opsx:propose <变更名称> 开始创建文档');
1088
+ console.log(' 1. 激活 opsx-propose skill,输入变更名称开始创建文档');
1150
1089
  console.log(' 2. 按顺序执行 propose → spec → design → task');
1151
- console.log(' 3. 运行 /opsx:check 验证文档质量');
1152
- console.log(' 4. 运行 /opsx:apply 申请实施,/opsx:archive 归档完成');
1090
+ console.log(' 3. 激活 opsx-check 验证文档质量');
1091
+ console.log(' 4. 激活 opsx-apply 申请实施,opsx-archive 归档完成');
1153
1092
  console.log();
1154
- console.log('📊 SDD Telemetry(嵌入在命令流程中,自动执行,无需配置):');
1093
+ console.log('📊 SDD Telemetry(嵌入在 skill 流程中,自动执行,无需配置):');
1155
1094
  console.log(' - 度量数据自动采集到 skywalk-sdd/events/');
1156
1095
  console.log(' - 运行 node skywalk-sdd/log.cjs metrics --project=. 查看四维度指标');
1157
1096
  console.log();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kld-sdd",
3
- "version": "2.4.14",
4
- "description": "KLD SDD OpenSpec 项目初始化工具 - 内置模版一键初始化",
3
+ "version": "2.4.15",
4
+ "description": "KLD SDD OpenSpec 项目初始化工具 - 一键部署 SDD skills",
5
5
  "main": "index.js",
6
6
  "bin": {
7
7
  "kld-sdd": "bin/kld-sdd-init.js",
@@ -0,0 +1,170 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * SDD Apply Gate Hook
4
+ *
5
+ * 在 apply 阶段执行前进行门禁检查:必须先完成 check 阶段,才能执行 apply。
6
+ * 拦截 PreToolUse(Write/Edit) 事件,阻断越界写入操作。
7
+ *
8
+ * 检查逻辑:
9
+ * 1. 检测当前是否存在活跃的 apply 阶段(state 文件)
10
+ * 2. 若存在,回溯 events 目录确认 check 阶段是否已完成
11
+ * 3. 若 check 未完成,阻断本次写入并提示用户先执行 /opsx:check
12
+ */
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+
16
+ // ── 工具函数 ──────────────────────────────────────────────
17
+
18
+ function readStdin() {
19
+ try {
20
+ return fs.readFileSync(0, 'utf8');
21
+ } catch {
22
+ return '';
23
+ }
24
+ }
25
+
26
+ function parseInput(raw) {
27
+ try {
28
+ return JSON.parse(raw || '{}');
29
+ } catch {
30
+ return {};
31
+ }
32
+ }
33
+
34
+ function hasTelemetryCli(dir) {
35
+ return fs.existsSync(path.join(dir, 'skywalk-sdd', 'log.cjs')) ||
36
+ fs.existsSync(path.join(dir, 'skywalk-sdd', 'log.js'));
37
+ }
38
+
39
+ function findProjectRoot(startDir) {
40
+ if (!startDir) return '';
41
+
42
+ let current = path.resolve(startDir);
43
+ while (true) {
44
+ if (hasTelemetryCli(current)) return current;
45
+ const parent = path.dirname(current);
46
+ if (parent === current) return '';
47
+ current = parent;
48
+ }
49
+ }
50
+
51
+ function getProjectRoot(input) {
52
+ const toolInput = input.tool_input || input.toolInput || {};
53
+ const candidates = [
54
+ toolInput.cwd,
55
+ input.cwd,
56
+ input.project_root,
57
+ process.env.CLAUDE_PROJECT_DIR,
58
+ process.env.PWD,
59
+ process.cwd(),
60
+ ].filter(Boolean);
61
+ for (const dir of candidates) {
62
+ const root = findProjectRoot(dir);
63
+ if (root) return root;
64
+ }
65
+ return process.cwd();
66
+ }
67
+
68
+ /**
69
+ * 获取当前活跃的 apply 阶段事件(从 state 目录)
70
+ */
71
+ function findActiveApplyStage(projectRoot) {
72
+ const stateDir = path.join(projectRoot, 'skywalk-sdd', 'state');
73
+ if (!fs.existsSync(stateDir)) return null;
74
+
75
+ const files = fs.readdirSync(stateDir).filter(f => f.endsWith('.json'));
76
+ for (const file of files) {
77
+ try {
78
+ const data = JSON.parse(fs.readFileSync(path.join(stateDir, file), 'utf8'));
79
+ const event = data.event || null;
80
+ if (event && event.command === 'apply') {
81
+ return event;
82
+ }
83
+ } catch {
84
+ // 跳过无法解析的 state 文件
85
+ }
86
+ }
87
+ return null;
88
+ }
89
+
90
+ /**
91
+ * 检查指定 change 是否已有完成的 check 阶段
92
+ * 在 events 目录中搜索 check 的 stage_end 事件
93
+ */
94
+ function hasCompletedCheck(projectRoot, changeName) {
95
+ const safeName = safeChangeName(changeName);
96
+ const eventsChangeDir = path.join(projectRoot, 'skywalk-sdd', 'data', 'events', safeName);
97
+ if (!fs.existsSync(eventsChangeDir)) return false;
98
+
99
+ const jsonlFiles = fs.readdirSync(eventsChangeDir)
100
+ .filter(f => f.endsWith('.jsonl'))
101
+ .sort()
102
+ .reverse(); // 从最新文件开始搜索
103
+
104
+ for (const file of jsonlFiles) {
105
+ try {
106
+ const lines = fs.readFileSync(path.join(eventsChangeDir, file), 'utf-8')
107
+ .split('\n')
108
+ .filter(Boolean);
109
+ // 倒序遍历,优先命中最近的事件
110
+ for (let i = lines.length - 1; i >= 0; i--) {
111
+ try {
112
+ const event = JSON.parse(lines[i]);
113
+ if (event.type === 'stage_end' && event.command === 'check' && event.result === 'success') {
114
+ return true;
115
+ }
116
+ } catch {
117
+ // 跳过无法解析的行
118
+ }
119
+ }
120
+ } catch {
121
+ // 跳过无法读取的文件
122
+ }
123
+ }
124
+ return false;
125
+ }
126
+
127
+ /**
128
+ * 将 change 名称转为安全的目录名(与 log.cjs 中 safeChangeName 逻辑一致)
129
+ */
130
+ function safeChangeName(name) {
131
+ if (!name) return 'general';
132
+ return String(name)
133
+ .replace(/[\\/:*?"<>|]/g, '-')
134
+ .replace(/[\s.]+/g, '-')
135
+ .replace(/-+/g, '-')
136
+ .replace(/^-|-$/g, '')
137
+ .toLowerCase() || 'general';
138
+ }
139
+
140
+ // ── 主逻辑 ─────────────────────────────────────────────────
141
+
142
+ const input = parseInput(readStdin());
143
+
144
+ // 仅拦截 Write 和 Edit 工具
145
+ const toolName = String(input.tool_name || input.toolName || '');
146
+ if (toolName !== 'Write' && toolName !== 'Edit') {
147
+ process.exit(0);
148
+ }
149
+
150
+ const projectRoot = getProjectRoot(input);
151
+
152
+ // 检查是否存在活跃的 apply 阶段
153
+ const activeApply = findActiveApplyStage(projectRoot);
154
+ if (!activeApply) {
155
+ // 没有活跃的 apply 阶段,无需门禁
156
+ process.exit(0);
157
+ }
158
+
159
+ // 检查 check 阶段是否已完成
160
+ if (hasCompletedCheck(projectRoot, activeApply.change)) {
161
+ // check 已完成,放行
162
+ process.exit(0);
163
+ }
164
+
165
+ // check 未完成,阻断写入
166
+ const changeName = activeApply.change || 'unknown';
167
+ console.log(JSON.stringify({
168
+ decision: 'block',
169
+ reason: `[SDD Apply Gate] 检测到 apply 阶段正在执行(change: ${changeName}),但 check 阶段尚未完成。\n\n请先执行 /opsx:check 完成质量门禁检查,再执行 /opsx:apply 进行代码实施。\n\n操作顺序:/opsx:check → /opsx:apply`,
170
+ }));
@@ -67,7 +67,7 @@ function readActiveStages(projectRoot) {
67
67
 
68
68
  const input = parseInput(readStdin());
69
69
  const prompt = input.prompt || input.message || input.user_prompt || '';
70
- if (!/\/opsx:/.test(prompt)) process.exit(0);
70
+ if (!/\/opsx[:|-]/.test(prompt)) process.exit(0);
71
71
 
72
72
  const projectRoot = findProjectRoot(input);
73
73
  const activeStages = readActiveStages(projectRoot)
@@ -78,7 +78,7 @@ const lines = [
78
78
  'SDD Telemetry reminder:',
79
79
  '- Run skywalk-sdd/log.cjs start before the OPSX stage work begins.',
80
80
  '- Run skywalk-sdd/log.cjs end before stopping the stage.',
81
- '- Hooks are only an enhancement; OPSX command instructions remain authoritative.',
81
+ '- Hooks are only an enhancement; OPSX skill instructions remain authoritative.',
82
82
  ];
83
83
 
84
84
  if (activeStages) {
@@ -19,6 +19,15 @@
19
19
  "command": "node \"${CLAUDE_PROJECT_DIR}/.claude/hooks/sdd-pre-tool.cjs\""
20
20
  }
21
21
  ]
22
+ },
23
+ {
24
+ "matcher": "Write|Edit",
25
+ "hooks": [
26
+ {
27
+ "type": "command",
28
+ "command": "node \"${CLAUDE_PROJECT_DIR}/.claude/hooks/sdd-apply-gate.cjs\""
29
+ }
30
+ ]
22
31
  }
23
32
  ],
24
33
  "PostToolUse": [