kld-sdd 2.4.13 → 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 无法被 / 菜单发现
@@ -599,6 +540,36 @@ function deployOpsxSkills(selectedTools = Object.keys(TOOL_CONFIGS)) {
599
540
  * 部署 Claude Code Hook Pack(可选增强)
600
541
  * Hook 只调用 skywalk-sdd/log.cjs,不写私有日志,不替代 OPSX 模板采集。
601
542
  */
543
+ function hookCommandText(hook) {
544
+ const args = Array.isArray(hook.args) ? hook.args : [];
545
+ return [hook.command, ...args].filter(Boolean).join(' ').replace(/\s+/g, ' ').trim();
546
+ }
547
+
548
+ function isSddClaudeHook(hook) {
549
+ const text = hookCommandText(hook);
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);
552
+ }
553
+
554
+ function sameHookCommand(left, right) {
555
+ return hookCommandText(left) === hookCommandText(right) &&
556
+ String(left.type || '') === String(right.type || '');
557
+ }
558
+
559
+ function removeManagedSddHooks(entries) {
560
+ return entries
561
+ .map(entry => {
562
+ if (!Array.isArray(entry.hooks)) {
563
+ return entry;
564
+ }
565
+ return {
566
+ ...entry,
567
+ hooks: entry.hooks.filter(hook => !isSddClaudeHook(hook)),
568
+ };
569
+ })
570
+ .filter(entry => !Array.isArray(entry.hooks) || entry.hooks.length > 0);
571
+ }
572
+
602
573
  function deployClaudeHookPack(selectedTools = Object.keys(TOOL_CONFIGS)) {
603
574
  if (!selectedTools.includes('claude')) {
604
575
  return true;
@@ -642,11 +613,14 @@ function deployClaudeHookPack(selectedTools = Object.keys(TOOL_CONFIGS)) {
642
613
 
643
614
  existingSettings.hooks = existingSettings.hooks || {};
644
615
  for (const [eventName, entries] of Object.entries(templateSettings.hooks || {})) {
645
- existingSettings.hooks[eventName] = existingSettings.hooks[eventName] || [];
616
+ existingSettings.hooks[eventName] = removeManagedSddHooks(existingSettings.hooks[eventName] || []);
646
617
  for (const entry of entries) {
647
- const templateCommands = (entry.hooks || []).map(h => h.command).filter(Boolean);
618
+ const templateHooks = entry.hooks || [];
648
619
  const exists = existingSettings.hooks[eventName].some(existingEntry => {
649
- return (existingEntry.hooks || []).some(h => templateCommands.includes(h.command));
620
+ const existingHooks = existingEntry.hooks || [];
621
+ return templateHooks.every(templateHook => {
622
+ return existingHooks.some(existingHook => sameHookCommand(existingHook, templateHook));
623
+ });
650
624
  });
651
625
  if (!exists) {
652
626
  existingSettings.hooks[eventName].push(entry);
@@ -656,7 +630,7 @@ function deployClaudeHookPack(selectedTools = Object.keys(TOOL_CONFIGS)) {
656
630
 
657
631
  fs.writeFileSync(targetSettingsPath, JSON.stringify(existingSettings, null, 2) + '\n', 'utf8');
658
632
  console.log(' ✓ 合并 .claude/settings.json hooks 配置');
659
- console.log('✅ Claude Code SDD Hook Pack 已就绪(可选增强,核心采集仍由 OPSX 模板完成)');
633
+ console.log('✅ Claude Code SDD Hook Pack 已就绪(可选增强,核心采集仍由 OPSX skills 完成)');
660
634
  return true;
661
635
  }
662
636
 
@@ -783,7 +757,7 @@ function initGlobalOverview() {
783
757
 
784
758
  console.log(`✅ 全局架构约束已初始化: ${targetFile}`);
785
759
  console.log(' ℹ️ 此文件定义了全局数据字典、接口规范、共享实体等约束');
786
- console.log(' ℹ️ 执行 /opsx:design 或 /opsx:task 时会自动读取此文件');
760
+ console.log(' ℹ️ 执行 opsx-design 或 opsx-task 时会自动读取此文件');
787
761
 
788
762
  return true;
789
763
  }
@@ -994,7 +968,7 @@ KLD SDD 项目初始化工具
994
968
 
995
969
  示例:
996
970
  kld-sdd-init # 完整初始化流程
997
- kld-sdd-init --skip-openspec # 跳过 openspec,直接部署命令
971
+ kld-sdd-init --skip-openspec # 跳过 openspec,直接部署 skills
998
972
  kld-sdd-init --tool codex # 仅部署 Codex 配置
999
973
  `);
1000
974
  }
@@ -1047,19 +1021,16 @@ async function main() {
1047
1021
  // 2. 清理原生 openspec 命令(删除 opsx-*.md,避免命名混淆)
1048
1022
  cleanupNativeOpenspecCommands(selectedTools);
1049
1023
 
1050
- // 3. 部署 SDD opsx 命令(根据编辑器类型使用不同模板)
1051
- overrideOpsxCommands(selectedTools);
1052
-
1053
- // 4. 清理旧版嵌套 skills/kld-sdd/ bundle(Claude Code 无法识别二层目录)
1024
+ // 3. 清理旧版嵌套 skills/kld-sdd/ bundle(Claude Code 无法识别二层目录)
1054
1025
  cleanupLegacyBundledOpsxSkills(selectedTools);
1055
1026
 
1056
- // 4.1 部署 SDD opsx skills(扁平到 .claude/skills/opsx-*/)
1027
+ // 3.1 部署 SDD opsx skills(扁平到 .claude/skills/opsx-*/)
1057
1028
  deployOpsxSkills(selectedTools);
1058
1029
 
1059
- // 4.2 清理原生 openspec-* skills(防止残留)
1030
+ // 3.2 清理原生 openspec-* skills(防止残留)
1060
1031
  cleanupNativeOpenspecSkills(selectedTools);
1061
1032
 
1062
- // 4.2 部署 Claude Code Hook Pack(可选增强)
1033
+ // 3.3 部署 Claude Code Hook Pack(可选增强)
1063
1034
  deployClaudeHookPack(selectedTools);
1064
1035
 
1065
1036
  // 5. 部署 SDD Telemetry 数据目录
@@ -1090,7 +1061,7 @@ async function main() {
1090
1061
  console.log('已生成/覆盖:');
1091
1062
  console.log(' 🌐 openspec/specs/overview.md # 全局架构约束(数据字典、接口规范)');
1092
1063
  console.log(' 📄 openspec-templates/ # 标准文档模版(参考用)');
1093
- console.log(' 🔧 .*/commands/opsx/ # 9 个 SDD 命令');
1064
+ console.log(' 🎯 .*/skills/opsx-*/ # SDD skills(扁平一层)');
1094
1065
  if (selectedTools.includes('claude')) {
1095
1066
  console.log(' 🪝 .claude/hooks/ # Claude Code SDD Hook Pack(可选增强)');
1096
1067
  }
@@ -1099,26 +1070,27 @@ async function main() {
1099
1070
  console.log(' 🧪 skywalk-sdd/ci/ # CI 兜底采集模板');
1100
1071
  console.log();
1101
1072
 
1102
- // 统一的命令格式说明
1103
- console.log('可用命令(/opsx:* 系列,9 个 SDD 命令):');;
1104
- console.log(' /opsx:propose - 创建业务意图文档(Why)');
1105
- console.log(' /opsx:spec - 创建技术契约文档(What)');
1106
- console.log(' /opsx:design - 创建技术实现方案(How)');
1107
- console.log(' /opsx:task - 拆解AI编码任务(Do)');
1108
- console.log(' /opsx:check - 质量门禁检查(Verify)');
1109
- console.log(' /opsx:apply - 申请变更实施(Apply)');
1110
- console.log(' /opsx:test - 执行单元测试(Test)');
1111
- console.log(' /opsx:archive - 归档变更(Archive)');
1112
- 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 - 业务知识库检索(辅助)');
1113
1085
 
1114
1086
  console.log();
1115
1087
  console.log('后续步骤:');
1116
- console.log(' 1. 运行 /opsx:propose <变更名称> 开始创建文档');
1088
+ console.log(' 1. 激活 opsx-propose skill,输入变更名称开始创建文档');
1117
1089
  console.log(' 2. 按顺序执行 propose → spec → design → task');
1118
- console.log(' 3. 运行 /opsx:check 验证文档质量');
1119
- console.log(' 4. 运行 /opsx:apply 申请实施,/opsx:archive 归档完成');
1090
+ console.log(' 3. 激活 opsx-check 验证文档质量');
1091
+ console.log(' 4. 激活 opsx-apply 申请实施,opsx-archive 归档完成');
1120
1092
  console.log();
1121
- console.log('📊 SDD Telemetry(嵌入在命令流程中,自动执行,无需配置):');
1093
+ console.log('📊 SDD Telemetry(嵌入在 skill 流程中,自动执行,无需配置):');
1122
1094
  console.log(' - 度量数据自动采集到 skywalk-sdd/events/');
1123
1095
  console.log(' - 运行 node skywalk-sdd/log.cjs metrics --project=. 查看四维度指标');
1124
1096
  console.log();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kld-sdd",
3
- "version": "2.4.13",
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",
@@ -39,11 +39,18 @@ function main() {
39
39
  });
40
40
 
41
41
  if (result.error) {
42
- console.error(`SDD pre-push: failed to run doctor: ${result.error.message}`);
43
- process.exit(1);
42
+ console.warn(`SDD pre-push: failed to run doctor: ${result.error.message}; push will continue.`);
43
+ return;
44
44
  }
45
45
 
46
- process.exit(result.status || 0);
46
+ if (result.status && result.status !== 0) {
47
+ console.warn(`SDD pre-push: doctor reported issues with exit code ${result.status}; push will continue.`);
48
+ return;
49
+ }
50
+
51
+ if (result.signal) {
52
+ console.warn(`SDD pre-push: doctor stopped by signal ${result.signal}; push will continue.`);
53
+ }
47
54
  }
48
55
 
49
56
  main();
@@ -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
+ }));