kld-sdd 2.4.19 → 2.5.1

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.
Files changed (47) hide show
  1. package/kld-sdd-guide.html +1109 -0
  2. package/lib/command-bridge.js +156 -0
  3. package/lib/deploy-codebuddy-hooks.js +99 -0
  4. package/lib/hook-gate-core.js +333 -0
  5. package/lib/init.js +136 -93
  6. package/lib/settings-merge.js +85 -0
  7. package/lib/skills-bundle.js +142 -5
  8. package/lib/tool-profiles.js +270 -0
  9. package/package.json +3 -2
  10. package/skywalk-sdd/index.cjs +1808 -129
  11. package/templates/commands/kunlunzhima/skill-bridge.md +23 -0
  12. package/templates/hooks/claude/hooks/sdd-apply-test-gate.cjs +175 -28
  13. package/templates/hooks/claude/hooks/sdd-post-tool.cjs +42 -21
  14. package/templates/hooks/codebuddy/hooks/sdd-apply-gate.cjs +16 -0
  15. package/templates/hooks/codebuddy/hooks/sdd-apply-test-gate.cjs +395 -0
  16. package/templates/hooks/codebuddy/hooks/sdd-post-tool.cjs +123 -0
  17. package/templates/hooks/codebuddy/hooks/sdd-pre-tool.cjs +16 -0
  18. package/templates/hooks/codebuddy/hooks/sdd-prompt.cjs +48 -0
  19. package/templates/hooks/codebuddy/hooks/sdd-skill-apply-gate.cjs +16 -0
  20. package/templates/hooks/codebuddy/hooks/sdd-stop.cjs +70 -0
  21. package/templates/hooks/codebuddy/settings.json +72 -0
  22. package/templates/openspec/proposal.md +0 -1
  23. package/templates/openspec/spec.md +2 -2
  24. package/templates/skills/kld-sdd/opsx-apply/SKILL.md +65 -356
  25. package/templates/skills/kld-sdd/opsx-apply/checklist.md +94 -0
  26. package/templates/skills/kld-sdd/opsx-apply/reference.md +403 -0
  27. package/templates/skills/kld-sdd/opsx-archive/SKILL.md +21 -5
  28. package/templates/skills/kld-sdd/opsx-archive/checklist.md +33 -0
  29. package/templates/skills/kld-sdd/opsx-check/SKILL.md +29 -5
  30. package/templates/skills/kld-sdd/opsx-check/checklist.md +37 -0
  31. package/templates/skills/kld-sdd/opsx-design/SKILL.md +47 -51
  32. package/templates/skills/kld-sdd/opsx-design/checklist.md +46 -0
  33. package/templates/skills/kld-sdd/opsx-design/reference.md +44 -0
  34. package/templates/skills/kld-sdd/opsx-explore/SKILL.md +1 -1
  35. package/templates/skills/kld-sdd/opsx-propose/SKILL.md +52 -96
  36. package/templates/skills/kld-sdd/opsx-propose/checklist.md +44 -0
  37. package/templates/skills/kld-sdd/opsx-propose/reference.md +94 -0
  38. package/templates/skills/kld-sdd/opsx-rules/SKILL.md +131 -0
  39. package/templates/skills/kld-sdd/opsx-rules/checklist.md +27 -0
  40. package/templates/skills/kld-sdd/opsx-rules/reference.md +124 -0
  41. package/templates/skills/kld-sdd/opsx-spec/SKILL.md +47 -51
  42. package/templates/skills/kld-sdd/opsx-spec/checklist.md +46 -0
  43. package/templates/skills/kld-sdd/opsx-spec/reference.md +49 -0
  44. package/templates/skills/kld-sdd/opsx-task/SKILL.md +43 -46
  45. package/templates/skills/kld-sdd/opsx-task/checklist.md +46 -0
  46. package/templates/skills/kld-sdd/opsx-task/reference.md +40 -0
  47. package/templates/skills/kld-sdd/opsx-test/SKILL.md +13 -1
package/lib/init.js CHANGED
@@ -17,83 +17,25 @@ const os = require('os');
17
17
  const {
18
18
  SKILLS_BUNDLE_NAME,
19
19
  OPSX_SKILL_DIRS,
20
- renderTemplate
20
+ OPSX_SKILL_METADATA,
21
+ renderTemplate,
22
+ validateRenderedContent,
21
23
  } = require('./skills-bundle');
24
+ const { getToolProfile, buildToolConfigs } = require('./tool-profiles');
25
+ const {
26
+ deployKunlunCommandBridges,
27
+ cleanupManagedKunlunBridges,
28
+ BRIDGE_MARKER,
29
+ } = require('./command-bridge');
30
+ const { deployCodebuddyHookPack } = require('./deploy-codebuddy-hooks');
22
31
 
23
32
  const rl = readline.createInterface({
24
33
  input: process.stdin,
25
34
  output: process.stdout
26
35
  });
27
36
 
28
- // 工具类型配置
29
- const TOOL_CONFIGS = {
30
- cursor: {
31
- name: 'Cursor',
32
- configDir: '.cursor',
33
- commandsDir: '.cursor/commands',
34
- opsxDir: '.cursor/commands/opsx',
35
- skillsDir: '.cursor/skills',
36
- agentType: 'cursor'
37
- },
38
- claude: {
39
- name: 'Claude Code',
40
- configDir: '.claude',
41
- commandsDir: '.claude/commands',
42
- opsxDir: '.claude/commands/opsx',
43
- skillsDir: '.claude/skills',
44
- agentType: 'claude-code'
45
- },
46
- codebuddy: {
47
- name: 'CodeBuddy',
48
- configDir: '.codebuddy',
49
- commandsDir: '.codebuddy/commands',
50
- opsxDir: '.codebuddy/commands/opsx',
51
- skillsDir: '.codebuddy/skills',
52
- agentType: 'codebuddy'
53
- },
54
- qoder: {
55
- name: 'Qoder',
56
- configDir: '.qoder',
57
- commandsDir: '.qoder/commands',
58
- opsxDir: '.qoder/commands/opsx',
59
- skillsDir: '.qoder/skills',
60
- agentType: 'qoder'
61
- },
62
- opencode: {
63
- name: 'OpenCode',
64
- configDir: '.opencode',
65
- commandsDir: '.opencode/commands',
66
- opsxDir: '.opencode/commands/opsx',
67
- skillsDir: '.opencode/skills',
68
- agentType: 'opencode',
69
- // opencode 原生命令使用 command/(单数),需要额外清理
70
- nativeCommandDir: '.opencode/command'
71
- },
72
- kunlunzhima: {
73
- name: 'KunlunZhima',
74
- configDir: '.kunlunzhima',
75
- commandsDir: '.kunlunzhima/commands',
76
- opsxDir: '.kunlunzhima/commands/opsx',
77
- skillsDir: '.kunlunzhima/skills',
78
- agentType: 'kunlunzhima'
79
- },
80
- workbuddy: {
81
- name: 'WorkBuddy',
82
- configDir: '.workbuddy',
83
- commandsDir: '.workbuddy/commands',
84
- opsxDir: '.workbuddy/commands/opsx',
85
- skillsDir: '.workbuddy/skills',
86
- agentType: 'workbuddy'
87
- },
88
- codex: {
89
- name: 'Codex',
90
- configDir: '.agents',
91
- commandsDir: '.agents/commands',
92
- opsxDir: '.agents/commands/opsx',
93
- skillsDir: '.agents/skills',
94
- agentType: 'codex'
95
- }
96
- };
37
+ // 工具类型配置(路径字段从 TOOL_PROFILES 派生,避免双份漂移)
38
+ const TOOL_CONFIGS = buildToolConfigs();
97
39
 
98
40
  /**
99
41
  * 询问用户选择
@@ -331,6 +273,8 @@ function copyDir(source, target) {
331
273
  }
332
274
 
333
275
  function copyDirRendered(source, target, config, toolKey) {
276
+ const profile = getToolProfile(toolKey);
277
+
334
278
  if (!fs.existsSync(target)) {
335
279
  fs.mkdirSync(target, { recursive: true });
336
280
  }
@@ -344,7 +288,8 @@ function copyDirRendered(source, target, config, toolKey) {
344
288
  if (stat.isDirectory()) {
345
289
  copyDirRendered(srcPath, tgtPath, config, toolKey);
346
290
  } else {
347
- const rendered = renderTemplate(fs.readFileSync(srcPath, 'utf8'), config, toolKey);
291
+ const rendered = renderTemplate(fs.readFileSync(srcPath, 'utf8'), config, toolKey, profile);
292
+ validateRenderedContent(rendered, profile, tgtPath);
348
293
  fs.writeFileSync(tgtPath, rendered, 'utf8');
349
294
  }
350
295
  }
@@ -363,7 +308,9 @@ const NATIVE_OPSX_CMDS_SLASH = ['propose.md', 'apply.md', 'archive.md', 'explore
363
308
  * SDD 自定义 command 标记(init 不部署;用户手动维护时保留)
364
309
  */
365
310
  function isSddCustomCommand(content) {
366
- return content.includes('# SDD') || content.includes('skywalk-sdd/log.cjs');
311
+ return content.includes(BRIDGE_MARKER)
312
+ || content.includes('skywalk-sdd/log.cjs')
313
+ || /# SDD\b/.test(content);
367
314
  }
368
315
 
369
316
  /**
@@ -376,7 +323,8 @@ function isSddCustomCommand(content) {
376
323
  * openspec 对不同工具生成的目录结构:
377
324
  * - Cursor: commands/opsx-*.md(根目录,连字符)
378
325
  * - Claude/CodeBuddy/Qoder 等: commands/opsx/*.md(opsx 子目录)
379
- * - OpenCode: command/opsx-*.md(command 单数目录,整目录删除)
326
+ * - OpenSpec OpenCode: commands/opsx-*.md(复数扁平,通用 commands/ 循环清理)
327
+ * - Legacy OpenCode: command/(单数目录,nativeCommandDir 整目录删除)
380
328
  *
381
329
  * @param {string[]} selectedTools - 用户选择的编辑器列表
382
330
  */
@@ -524,16 +472,17 @@ function deployOpsxSkills(selectedTools = Object.keys(TOOL_CONFIGS)) {
524
472
 
525
473
  for (const toolKey of selectedTools) {
526
474
  const config = TOOL_CONFIGS[toolKey];
527
- if (!config || !config.skillsDir) continue;
475
+ const profile = getToolProfile(toolKey);
476
+ if (!config || !profile.skillsDir) continue;
528
477
 
529
- const targetSkillsDir = path.join(cwd, config.skillsDir);
478
+ const targetSkillsDir = path.join(cwd, profile.skillsDir);
530
479
 
531
480
  // 确保 skills 目录存在
532
481
  if (!fs.existsSync(targetSkillsDir)) {
533
482
  fs.mkdirSync(targetSkillsDir, { recursive: true });
534
483
  }
535
484
 
536
- console.log(` 部署 ${config.name} 的 opsx skills(${skillDirs.length} 个,扁平到 ${config.skillsDir}/)...`);
485
+ console.log(` 部署 ${config.name} 的 opsx skills(${skillDirs.length} 个,扁平到 ${profile.skillsDir}/)...`);
537
486
 
538
487
  for (const skillDir of skillDirs) {
539
488
  const sourceDir = path.join(skillsTemplatePath, skillDir);
@@ -554,6 +503,46 @@ function deployOpsxSkills(selectedTools = Object.keys(TOOL_CONFIGS)) {
554
503
  return true;
555
504
  }
556
505
 
506
+ /**
507
+ * 按 profile 部署 Agent 专属产物(Kunlun bridge / CodeBuddy hooks / Claude hooks)
508
+ * 仅处理 selectedTools 中的 profile,不触碰未选 Agent 目录。
509
+ */
510
+ function deployProfileArtifacts(selectedTools = Object.keys(TOOL_CONFIGS), cwd = process.cwd(), pkgPath = getPackagePath()) {
511
+ console.log('🧩 正在部署 Agent 专属产物...');
512
+
513
+ for (const toolKey of selectedTools) {
514
+ const profile = getToolProfile(toolKey);
515
+ const config = TOOL_CONFIGS[toolKey];
516
+ if (!config) continue;
517
+
518
+ if (profile.requiresCommandBridge) {
519
+ cleanupManagedKunlunBridges(profile, cwd);
520
+ const bridgeResult = deployKunlunCommandBridges({
521
+ profile,
522
+ projectRoot: cwd,
523
+ pkgPath,
524
+ skillMetadata: OPSX_SKILL_METADATA,
525
+ });
526
+ console.log(` ✓ ${config.name}: 部署 ${bridgeResult.deployed.length} 个 Kunlun command bridge`);
527
+ }
528
+
529
+ if (profile.hookProvider === 'codebuddy') {
530
+ const hookResult = deployCodebuddyHookPack(profile, cwd, pkgPath);
531
+ if (!hookResult.ok) {
532
+ throw new Error(hookResult.error || `CodeBuddy Hook 部署失败: ${profile.configDir}/settings.json`);
533
+ }
534
+ console.log(` ✓ ${config.name}: 部署 Hook Pack(运行 /hooks 审核后生效)`);
535
+ }
536
+
537
+ if (profile.hookProvider === 'claude') {
538
+ deployClaudeHookPack([toolKey]);
539
+ }
540
+ }
541
+
542
+ console.log('✅ Agent 专属产物部署完成');
543
+ return true;
544
+ }
545
+
557
546
  /**
558
547
  * 部署 Claude Code Hook Pack(可选增强)
559
548
  * Hook 只调用 skywalk-sdd/log.cjs,不写私有日志,不替代 OPSX 模板采集。
@@ -780,9 +769,33 @@ function initGlobalOverview() {
780
769
  return true;
781
770
  }
782
771
 
772
+ /**
773
+ * 部署 SDD 操作手册(HTML)到 openspec 目录
774
+ */
775
+ function deploySddGuideManual(cwd = process.cwd(), pkgPath = getPackagePath()) {
776
+ console.log('📖 正在部署 SDD 操作手册...');
777
+
778
+ const source = path.join(pkgPath, 'kld-sdd-guide.html');
779
+ const targetDir = path.join(cwd, 'openspec');
780
+ const targetFile = path.join(targetDir, 'kld-sdd操作手册.html');
781
+
782
+ if (!fs.existsSync(source)) {
783
+ console.log('⚠️ 操作手册源文件不存在,跳过: kld-sdd-guide.html');
784
+ return false;
785
+ }
786
+
787
+ if (!fs.existsSync(targetDir)) {
788
+ fs.mkdirSync(targetDir, { recursive: true });
789
+ }
790
+
791
+ fs.copyFileSync(source, targetFile);
792
+ console.log(`✅ SDD 操作手册已部署: openspec/kld-sdd操作手册.html`);
793
+ return true;
794
+ }
795
+
783
796
  /**
784
797
  * 部署 SDD Telemetry 数据目录
785
- * 创建本地数据目录用于存储事件和报告
798
+ * 创建本地数据目录用于存储事件和活跃状态(最终报告落 openspec/changes/<change>/reports/,不再写 skywalk-sdd/reports/)
786
799
  */
787
800
  function deployTelemetryDataDir() {
788
801
  console.log('📊 正在初始化 SDD Telemetry 数据目录...');
@@ -795,18 +808,13 @@ function deployTelemetryDataDir() {
795
808
  fs.mkdirSync(dataDir, { recursive: true });
796
809
  }
797
810
 
798
- // 创建本地数据目录(事件和报告存储)
811
+ // 创建本地数据目录(事件和活跃状态存储;报告落 change 子目录,不在此创建)
799
812
  const eventsDir = path.join(dataDir, 'events');
800
- const reportsDir = path.join(dataDir, 'reports');
801
813
  const stateDir = path.join(dataDir, 'state');
802
814
  if (!fs.existsSync(eventsDir)) {
803
815
  fs.mkdirSync(eventsDir, { recursive: true });
804
816
  console.log(' ✓ 创建 skywalk-sdd/events/ 数据目录');
805
817
  }
806
- if (!fs.existsSync(reportsDir)) {
807
- fs.mkdirSync(reportsDir, { recursive: true });
808
- console.log(' ✓ 创建 skywalk-sdd/reports/ 报告目录');
809
- }
810
818
  if (!fs.existsSync(stateDir)) {
811
819
  fs.mkdirSync(stateDir, { recursive: true });
812
820
  console.log(' ✓ 创建 skywalk-sdd/state/ 活跃阶段状态目录');
@@ -876,10 +884,14 @@ function installGitHookShim(cwd, hookName, scriptPath) {
876
884
  * 这些脚本只做质量门禁和 CI 补充记录,不替代 OPSX 主采集链路。
877
885
  */
878
886
  function deployQualityGateTemplates() {
879
- console.log('🧰 正在部署 SDD Git hooks / CI 兜底模板...');
880
-
881
887
  const pkgPath = getPackagePath();
882
888
  const cwd = process.cwd();
889
+ // U4: 非 Git 项目跳过 Git hooks / CI 兜底模板(按需部署,不污染非 Git 项目根)
890
+ if (!fs.existsSync(path.join(cwd, '.git'))) {
891
+ console.log('🧰 非 Git 项目,跳过 Git hooks / CI 兜底模板部署');
892
+ return false;
893
+ }
894
+ console.log('🧰 正在部署 SDD Git hooks / CI 兜底模板...');
883
895
  const dataDir = path.join(cwd, 'skywalk-sdd');
884
896
  const gitHooksTemplateDir = path.join(pkgPath, 'templates', 'git-hooks');
885
897
  const ciTemplateDir = path.join(pkgPath, 'templates', 'ci');
@@ -936,7 +948,6 @@ function updateGitignore() {
936
948
  '.workbuddy/commands/personal-*',
937
949
  '.agents/commands/personal-*',
938
950
  'skywalk-sdd/events/',
939
- 'skywalk-sdd/reports/',
940
951
  'skywalk-sdd/state/',
941
952
  '.worktrees/'
942
953
  ];
@@ -951,9 +962,8 @@ function updateGitignore() {
951
962
  .workbuddy/commands/personal-*
952
963
  .agents/commands/personal-*
953
964
 
954
- # SDD Telemetry 数据(本地度量,可选提交)
965
+ # SDD Telemetry 数据(本地度量,可选提交;最终报告落 openspec/changes/<change>/reports/ 随 change 提交)
955
966
  skywalk-sdd/events/
956
- skywalk-sdd/reports/
957
967
  skywalk-sdd/state/
958
968
 
959
969
  # SDD Apply 隔离 worktree(本地临时目录)
@@ -1039,7 +1049,7 @@ async function main() {
1039
1049
  } catch (error) {
1040
1050
  console.error(`❌ ${error.message}`);
1041
1051
  rl.close();
1042
- return;
1052
+ process.exit(1);
1043
1053
  }
1044
1054
 
1045
1055
  // 1. 运行 openspec init(传入用户选择的工具)
@@ -1060,8 +1070,8 @@ async function main() {
1060
1070
  // 3.2 清理原生 openspec-* skills(防止残留)
1061
1071
  cleanupNativeOpenspecSkills(selectedTools);
1062
1072
 
1063
- // 3.3 部署 Claude Code Hook Pack(可选增强)
1064
- deployClaudeHookPack(selectedTools);
1073
+ // 3.3 部署 Agent 专属产物(Kunlun bridge / CodeBuddy hooks / Claude hooks)
1074
+ deployProfileArtifacts(selectedTools, process.cwd(), getPackagePath());
1065
1075
 
1066
1076
  // 5. 部署 SDD Telemetry 数据目录
1067
1077
  deployTelemetryDataDir();
@@ -1074,6 +1084,9 @@ async function main() {
1074
1084
 
1075
1085
  // 7. 初始化全局架构约束 (overview.md)
1076
1086
  initGlobalOverview();
1087
+
1088
+ // 7.1 部署 SDD 操作手册(HTML)
1089
+ deploySddGuideManual();
1077
1090
  }
1078
1091
 
1079
1092
  // 7. 更新 .gitignore
@@ -1090,8 +1103,16 @@ async function main() {
1090
1103
  console.log();
1091
1104
  console.log('已生成/覆盖:');
1092
1105
  console.log(' 🌐 openspec/specs/overview.md # 全局架构约束(数据字典、接口规范)');
1106
+ console.log(' 📖 openspec/kld-sdd操作手册.html # SDD 操作手册(HTML)');
1093
1107
  console.log(' 📄 openspec-templates/ # 标准文档模版(参考用)');
1094
1108
  console.log(' 🎯 .*/skills/opsx-*/ # SDD skills(扁平一层)');
1109
+ if (selectedTools.includes('kunlunzhima')) {
1110
+ console.log(' 📎 .kunlunzhima/commands/opsx/ # KunlunZhima OPSX command bridge(11 个)');
1111
+ console.log(' ℹ️ KunlunZhima 通过 commands/skills 入口使用 SDD,未启用自动 Hook');
1112
+ }
1113
+ if (selectedTools.includes('codebuddy')) {
1114
+ console.log(' 🪝 .codebuddy/hooks/ # CodeBuddy SDD Hook Pack(运行 /hooks 审核后生效)');
1115
+ }
1095
1116
  if (selectedTools.includes('claude')) {
1096
1117
  console.log(' 🪝 .claude/hooks/ # Claude Code SDD Hook Pack(可选增强)');
1097
1118
  }
@@ -1101,7 +1122,7 @@ async function main() {
1101
1122
  console.log();
1102
1123
 
1103
1124
  // 统一的 skill 格式说明
1104
- console.log('可用 skills(opsx-* 系列,10 个 SDD skills):');
1125
+ console.log('可用 skills(opsx-* 系列,11 个 SDD skills):');
1105
1126
  console.log(' opsx-propose - 创建业务意图文档(Why)');
1106
1127
  console.log(' opsx-spec - 创建技术契约文档(What)');
1107
1128
  console.log(' opsx-design - 创建技术实现方案(How)');
@@ -1112,6 +1133,7 @@ async function main() {
1112
1133
  console.log(' opsx-archive - 归档变更(Archive)');
1113
1134
  console.log(' opsx-explore - 浏览变更状态(Explore)');
1114
1135
  console.log(' opsx-knowledge - 业务知识库检索(辅助)');
1136
+ console.log(' opsx-rules - 生成/更新 Agent 规则(辅助)');
1115
1137
 
1116
1138
  console.log();
1117
1139
  console.log('后续步骤:');
@@ -1119,6 +1141,15 @@ async function main() {
1119
1141
  console.log(' 2. 按顺序执行 propose → spec → design → task');
1120
1142
  console.log(' 3. 激活 opsx-check 验证文档质量');
1121
1143
  console.log(' 4. 激活 opsx-apply 申请实施,opsx-archive 归档完成');
1144
+ if (selectedTools.includes('codebuddy')) {
1145
+ console.log(' 5. CodeBuddy 用户:重启会话后运行 /hooks 审核 SDD Hook,审批后门禁生效');
1146
+ }
1147
+ if (selectedTools.includes('kunlunzhima')) {
1148
+ console.log(' 5. KunlunZhima 用户:在 Craft 模式使用 /opsx:propose 等 OPSX 命令(.kunlunzhima/commands/opsx/)');
1149
+ }
1150
+ if (selectedTools.includes('opencode')) {
1151
+ console.log(' 5. OpenCode 用户:用 /opsx-propose 或 /skills 启动 SDD(.opencode/skills/opsx-*/,skill 即斜杠命令)');
1152
+ }
1122
1153
  console.log();
1123
1154
  console.log('📊 SDD Telemetry(嵌入在 skill 流程中,自动执行,无需配置):');
1124
1155
  console.log(' - 度量数据自动采集到 skywalk-sdd/events/');
@@ -1128,4 +1159,16 @@ async function main() {
1128
1159
  rl.close();
1129
1160
  }
1130
1161
 
1131
- module.exports = { main, detectTools, getTemplatePath, selectEditor };
1162
+ module.exports = {
1163
+ main,
1164
+ detectTools,
1165
+ getTemplatePath,
1166
+ getPackagePath,
1167
+ selectEditor,
1168
+ deployOpsxSkills,
1169
+ deployProfileArtifacts,
1170
+ deploySddGuideManual,
1171
+ copyDirRendered,
1172
+ parseSelectedTools,
1173
+ TOOL_CONFIGS,
1174
+ };
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Conservative merge for CodeBuddy .codebuddy/settings.json hooks.
3
+ */
4
+
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+
8
+ const SDD_HOOK_COMMAND_RE = /(?:^|\s|["'])\$?CODEBUDDY_PROJECT_DIR[\\/]+\.codebuddy[\\/]+hooks[\\/]+sdd-[\w-]+\.cjs(?=$|\s|["'])/i;
9
+
10
+ function hookCommandText(hook) {
11
+ const args = Array.isArray(hook.args) ? hook.args : [];
12
+ return [hook.command, ...args].filter(Boolean).join(' ').replace(/\s+/g, ' ').trim();
13
+ }
14
+
15
+ function isSddCodebuddyHook(hook) {
16
+ return SDD_HOOK_COMMAND_RE.test(hookCommandText(hook));
17
+ }
18
+
19
+ function sameHookCommand(left, right) {
20
+ return hookCommandText(left) === hookCommandText(right) &&
21
+ String(left.type || '') === String(right.type || '');
22
+ }
23
+
24
+ function removeManagedSddHooks(entries) {
25
+ return entries
26
+ .map(entry => {
27
+ if (!Array.isArray(entry.hooks)) {
28
+ return entry;
29
+ }
30
+ return {
31
+ ...entry,
32
+ hooks: entry.hooks.filter(hook => !isSddCodebuddyHook(hook)),
33
+ };
34
+ })
35
+ .filter(entry => !Array.isArray(entry.hooks) || entry.hooks.length > 0);
36
+ }
37
+
38
+ function mergeCodebuddySettings(settingsPath, templateSettings) {
39
+ const displayPath = path.basename(settingsPath);
40
+ let originalBytes = '';
41
+ if (fs.existsSync(settingsPath)) {
42
+ originalBytes = fs.readFileSync(settingsPath, 'utf8');
43
+ }
44
+
45
+ let existingSettings = {};
46
+ if (originalBytes) {
47
+ try {
48
+ existingSettings = JSON.parse(originalBytes);
49
+ } catch {
50
+ return {
51
+ ok: false,
52
+ error: `${displayPath} 不是合法 JSON,请修复后重试(路径: ${settingsPath})`,
53
+ };
54
+ }
55
+ }
56
+
57
+ const merged = { ...existingSettings };
58
+ merged.hooks = merged.hooks || {};
59
+
60
+ for (const [eventName, entries] of Object.entries(templateSettings.hooks || {})) {
61
+ merged.hooks[eventName] = removeManagedSddHooks(merged.hooks[eventName] || []);
62
+ for (const entry of entries) {
63
+ const templateHooks = entry.hooks || [];
64
+ const exists = merged.hooks[eventName].some(existingEntry => {
65
+ const existingHooks = existingEntry.hooks || [];
66
+ return templateHooks.every(templateHook =>
67
+ existingHooks.some(existingHook => sameHookCommand(existingHook, templateHook))
68
+ );
69
+ });
70
+ if (!exists) {
71
+ merged.hooks[eventName].push(entry);
72
+ }
73
+ }
74
+ }
75
+
76
+ fs.writeFileSync(settingsPath, JSON.stringify(merged, null, 2) + '\n', 'utf8');
77
+ return { ok: true };
78
+ }
79
+
80
+ module.exports = {
81
+ mergeCodebuddySettings,
82
+ isSddCodebuddyHook,
83
+ hookCommandText,
84
+ removeManagedSddHooks,
85
+ };
@@ -2,6 +2,9 @@
2
2
  * SDD skills bundle 共享配置与模板渲染
3
3
  */
4
4
 
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+
5
8
  const SKILLS_BUNDLE_NAME = 'kld-sdd';
6
9
 
7
10
  const OPSX_SKILL_DIRS = [
@@ -14,17 +17,151 @@ const OPSX_SKILL_DIRS = [
14
17
  'opsx-test',
15
18
  'opsx-archive',
16
19
  'opsx-explore',
17
- 'opsx-knowledge'
20
+ 'opsx-knowledge',
21
+ 'opsx-rules',
18
22
  ];
19
23
 
20
- function renderTemplate(content, config, toolKey) {
24
+ const PKG_ROOT = path.resolve(__dirname, '..');
25
+
26
+ function commandNameFromSkillDir(skillDir) {
27
+ return skillDir.replace(/^opsx-/, '');
28
+ }
29
+
30
+ function displayNameFromCommandName(commandName) {
31
+ return `OPSX: ${commandName.charAt(0).toUpperCase() + commandName.slice(1)}`;
32
+ }
33
+
34
+ function parseSkillFrontmatter(content) {
35
+ const stripped = content.replace(/^\uFEFF/, '');
36
+ if (!stripped.startsWith('---')) {
37
+ return {};
38
+ }
39
+
40
+ const end = stripped.indexOf('---', 3);
41
+ if (end < 0) {
42
+ return {};
43
+ }
44
+
45
+ const block = stripped.slice(3, end).trim();
46
+ const result = {};
47
+
48
+ for (const rawLine of block.split('\n')) {
49
+ const line = rawLine.replace(/\r$/, '');
50
+ const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
51
+ if (!match) continue;
52
+ const key = match[1];
53
+ let value = match[2].trim();
54
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
55
+ value = value.slice(1, -1);
56
+ }
57
+ result[key] = value;
58
+ }
59
+
60
+ return result;
61
+ }
62
+
63
+ function buildOpsxSkillMetadata(pkgPath = PKG_ROOT) {
64
+ const skillsTemplateDir = path.join(pkgPath, 'templates', 'skills', SKILLS_BUNDLE_NAME);
65
+
66
+ return OPSX_SKILL_DIRS.map((skillDir) => {
67
+ const commandName = commandNameFromSkillDir(skillDir);
68
+ const skillMdPath = path.join(skillsTemplateDir, skillDir, 'SKILL.md');
69
+ let description = '';
70
+ let argumentHint = '';
71
+
72
+ if (fs.existsSync(skillMdPath)) {
73
+ const frontmatter = parseSkillFrontmatter(fs.readFileSync(skillMdPath, 'utf8'));
74
+ description = frontmatter.description || '';
75
+ argumentHint = frontmatter['argument-hint'] || '';
76
+ }
77
+
78
+ return {
79
+ skillDir,
80
+ commandName,
81
+ displayName: displayNameFromCommandName(commandName),
82
+ description,
83
+ argumentHint,
84
+ };
85
+ });
86
+ }
87
+
88
+ const OPSX_SKILL_METADATA = buildOpsxSkillMetadata(PKG_ROOT);
89
+
90
+ const KUNLUN_SLASH_COMMANDS = [
91
+ 'propose',
92
+ 'spec',
93
+ 'design',
94
+ 'task',
95
+ 'check',
96
+ 'apply',
97
+ 'test',
98
+ 'archive',
99
+ 'explore',
100
+ ];
101
+
102
+ function transformKunlunSlashCommands(content) {
103
+ if (typeof content !== 'string') {
104
+ return content;
105
+ }
106
+
107
+ let result = content;
108
+ for (const cmd of KUNLUN_SLASH_COMMANDS) {
109
+ result = result.replace(new RegExp(`/opsx-${cmd}(?![a-z-])`, 'g'), `/opsx:${cmd}`);
110
+ }
111
+ return result;
112
+ }
113
+
114
+ function replaceProfileTokens(content, profile) {
115
+ if (!profile) {
116
+ return content;
117
+ }
118
+
119
+ const vars = profile.templateVariables || {};
21
120
  return content
22
- .replace(/^\uFEFF/, '')
23
- .replace(/<Agent(?:\u7c7b\u578b|\u7eeb\u8bf2\u7037)>/g, config.agentType || toolKey);
121
+ .replace(/\$\{SKILL_RUNTIME_DIR\}/g, vars.SKILL_RUNTIME_DIR || profile.skillsDir || '')
122
+ .replace(/\$\{HOOK_GATE_DESCRIPTION\}/g, vars.HOOK_GATE_DESCRIPTION || '')
123
+ .replace(/\$\{SHELL_GUIDANCE\}/g, vars.SHELL_GUIDANCE || '')
124
+ .replace(/\$\{AGENT_SKILL_DIR\}/g, profile.skillsDir || '');
125
+ }
126
+
127
+ function renderTemplate(content, config, toolKey, profile) {
128
+ let rendered = replaceProfileTokens(
129
+ content
130
+ .replace(/^\uFEFF/, '')
131
+ .replace(/<Agent(?:\u7c7b\u578b|\u7eeb\u8bf2\u7037)>/g, config.agentType || toolKey),
132
+ profile
133
+ );
134
+
135
+ if (toolKey === 'kunlunzhima') {
136
+ rendered = transformKunlunSlashCommands(rendered);
137
+ }
138
+
139
+ return rendered;
140
+ }
141
+
142
+ function validateRenderedContent(content, profile, context = '') {
143
+ const unresolved = (content.match(/\$\{[A-Z_]+\}/g) || [])
144
+ // Claude Code 运行时注入,部署时保留字面量
145
+ .filter((token) => token !== '${CLAUDE_SKILL_DIR}');
146
+ if (unresolved.length > 0) {
147
+ const unique = [...new Set(unresolved)];
148
+ const prefix = context ? `${context}: ` : '';
149
+ throw new Error(`${prefix}未解析的模板 token: ${unique.join(', ')}`);
150
+ }
151
+
152
+ return true;
24
153
  }
25
154
 
26
155
  module.exports = {
27
156
  SKILLS_BUNDLE_NAME,
28
157
  OPSX_SKILL_DIRS,
29
- renderTemplate
158
+ OPSX_SKILL_METADATA,
159
+ buildOpsxSkillMetadata,
160
+ commandNameFromSkillDir,
161
+ displayNameFromCommandName,
162
+ parseSkillFrontmatter,
163
+ renderTemplate,
164
+ replaceProfileTokens,
165
+ transformKunlunSlashCommands,
166
+ validateRenderedContent,
30
167
  };