kld-sdd 2.4.14 → 2.4.16
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 +32 -47
- package/lib/init.js +30 -91
- package/package.json +2 -2
- package/templates/hooks/claude/hooks/sdd-apply-gate.cjs +171 -0
- package/templates/hooks/claude/hooks/sdd-pre-tool.cjs +3 -0
- package/templates/hooks/claude/hooks/sdd-prompt.cjs +2 -2
- package/templates/hooks/claude/settings.json +9 -0
- package/templates/skills/kld-sdd/opsx-apply/SKILL.md +132 -9
- package/templates/skills/kld-sdd/opsx-apply/implementer-prompt.md +129 -0
- package/templates/skills/kld-sdd/opsx-apply/worktree-setup.md +141 -0
- package/templates/opsx-commands/apply.md +0 -490
- package/templates/opsx-commands/archive.md +0 -146
- package/templates/opsx-commands/check.md +0 -463
- package/templates/opsx-commands/design.md +0 -570
- package/templates/opsx-commands/explore.md +0 -99
- package/templates/opsx-commands/propose.md +0 -405
- package/templates/opsx-commands/spec.md +0 -526
- package/templates/opsx-commands/task.md +0 -433
- package/templates/opsx-commands/test.md +0 -244
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# kld-sdd
|
|
2
2
|
|
|
3
|
-
KLD SDD OpenSpec 项目初始化工具 - 一键初始化 AI
|
|
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
|
|
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
|
|
18
|
+
初始化完成后,打开你的 AI 编辑器,激活 `opsx-*` skills 开始工作。
|
|
19
19
|
|
|
20
20
|
---
|
|
21
21
|
|
|
22
|
-
## 核心工作流:5
|
|
22
|
+
## 核心工作流:5 个 skills
|
|
23
23
|
|
|
24
|
-
初始化后,你会得到以下
|
|
24
|
+
初始化后,你会得到以下 SDD skills,按顺序使用:
|
|
25
25
|
|
|
26
26
|
```
|
|
27
|
-
propose → spec → design → task → check
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
opsx-propose → opsx-spec → opsx-design → opsx-task → opsx-check
|
|
28
|
+
↓
|
|
29
|
+
opsx-knowledge(独立使用)
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
###
|
|
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
|
-
###
|
|
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
|
-
###
|
|
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
|
-
###
|
|
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
|
-
###
|
|
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
|
-
###
|
|
122
|
+
### 独立使用:`opsx-knowledge`
|
|
123
123
|
|
|
124
|
-
|
|
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 |
|
|
215
|
-
| Claude Code |
|
|
216
|
-
| CodeBuddy |
|
|
217
|
-
| Qoder |
|
|
218
|
-
| OpenCode |
|
|
219
|
-
| KunlunZhima |
|
|
220
|
-
| WorkBuddy |
|
|
221
|
-
| Codex |
|
|
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/
|
|
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.
|
|
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
|
-
|
|
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/
|
|
261
|
+
A:不用担心,`kld-sdd-init` 会自动创建 `.codebuddy/skills/`、`.agents/skills/` 等目录。
|
|
277
262
|
|
|
278
|
-
**Q
|
|
263
|
+
**Q:`opsx-propose` 和直接让 AI 写代码有什么区别?**
|
|
279
264
|
|
|
280
265
|
A:直接让 AI 写代码,AI 会凭空假设很多细节,导致返工。SDD 流程强制先把"要做什么"写清楚,AI 按契约实现,减少歧义和返工。
|
|
281
266
|
|
|
282
267
|
**Q:团队规范已经有了,怎么让 AI 每次都遵守?**
|
|
283
268
|
|
|
284
|
-
A
|
|
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.
|
|
8
|
-
* 4.
|
|
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(' ℹ️ 执行
|
|
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.
|
|
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
|
-
//
|
|
1027
|
+
// 3.1 部署 SDD opsx skills(扁平到 .claude/skills/opsx-*/)
|
|
1090
1028
|
deployOpsxSkills(selectedTools);
|
|
1091
1029
|
|
|
1092
|
-
//
|
|
1030
|
+
// 3.2 清理原生 openspec-* skills(防止残留)
|
|
1093
1031
|
cleanupNativeOpenspecSkills(selectedTools);
|
|
1094
1032
|
|
|
1095
|
-
//
|
|
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('
|
|
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('
|
|
1137
|
-
console.log('
|
|
1138
|
-
console.log('
|
|
1139
|
-
console.log('
|
|
1140
|
-
console.log('
|
|
1141
|
-
console.log('
|
|
1142
|
-
console.log('
|
|
1143
|
-
console.log('
|
|
1144
|
-
console.log('
|
|
1145
|
-
console.log('
|
|
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.
|
|
1088
|
+
console.log(' 1. 激活 opsx-propose skill,输入变更名称开始创建文档');
|
|
1150
1089
|
console.log(' 2. 按顺序执行 propose → spec → design → task');
|
|
1151
|
-
console.log(' 3.
|
|
1152
|
-
console.log(' 4.
|
|
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
|
@@ -0,0 +1,171 @@
|
|
|
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', '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
|
+
}));
|
|
171
|
+
process.exit(2);
|
|
@@ -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
|
|
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
|
|
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": [
|