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 +32 -47
- package/lib/init.js +64 -92
- package/package.json +2 -2
- package/templates/git-hooks/pre-push-sdd-check.cjs +10 -3
- package/templates/hooks/claude/hooks/sdd-apply-gate.cjs +170 -0
- package/templates/hooks/claude/hooks/sdd-post-tool.cjs +29 -3
- package/templates/hooks/claude/hooks/sdd-prompt.cjs +25 -3
- package/templates/hooks/claude/hooks/sdd-stop.cjs +29 -3
- package/templates/hooks/claude/settings.json +13 -4
- package/templates/skills/kld-sdd/opsx-apply/SKILL.md +91 -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 无法被 / 菜单发现
|
|
@@ -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
|
|
618
|
+
const templateHooks = entry.hooks || [];
|
|
648
619
|
const exists = existingSettings.hooks[eventName].some(existingEntry => {
|
|
649
|
-
|
|
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(' ℹ️ 执行
|
|
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.
|
|
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
|
-
//
|
|
1027
|
+
// 3.1 部署 SDD opsx skills(扁平到 .claude/skills/opsx-*/)
|
|
1057
1028
|
deployOpsxSkills(selectedTools);
|
|
1058
1029
|
|
|
1059
|
-
//
|
|
1030
|
+
// 3.2 清理原生 openspec-* skills(防止残留)
|
|
1060
1031
|
cleanupNativeOpenspecSkills(selectedTools);
|
|
1061
1032
|
|
|
1062
|
-
//
|
|
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('
|
|
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('
|
|
1104
|
-
console.log('
|
|
1105
|
-
console.log('
|
|
1106
|
-
console.log('
|
|
1107
|
-
console.log('
|
|
1108
|
-
console.log('
|
|
1109
|
-
console.log('
|
|
1110
|
-
console.log('
|
|
1111
|
-
console.log('
|
|
1112
|
-
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 - 业务知识库检索(辅助)');
|
|
1113
1085
|
|
|
1114
1086
|
console.log();
|
|
1115
1087
|
console.log('后续步骤:');
|
|
1116
|
-
console.log(' 1.
|
|
1088
|
+
console.log(' 1. 激活 opsx-propose skill,输入变更名称开始创建文档');
|
|
1117
1089
|
console.log(' 2. 按顺序执行 propose → spec → design → task');
|
|
1118
|
-
console.log(' 3.
|
|
1119
|
-
console.log(' 4.
|
|
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
|
@@ -39,11 +39,18 @@ function main() {
|
|
|
39
39
|
});
|
|
40
40
|
|
|
41
41
|
if (result.error) {
|
|
42
|
-
console.
|
|
43
|
-
|
|
42
|
+
console.warn(`SDD pre-push: failed to run doctor: ${result.error.message}; push will continue.`);
|
|
43
|
+
return;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
|
|
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
|
+
}));
|