kld-sdd 2.7.8-2 → 2.7.8-4

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 (41) hide show
  1. package/README.md +2 -0
  2. package/USABILITY.md +52 -0
  3. package/bin/kld-sdd-init.js +700 -34
  4. package/lib/command-bridge.js +26 -17
  5. package/lib/deploy-codebuddy-hooks.js +19 -26
  6. package/lib/deploy-strategy.js +293 -0
  7. package/lib/hook-gate-core.js +7 -8
  8. package/lib/hook-install.js +75 -0
  9. package/lib/init.js +450 -711
  10. package/lib/managed-hook-hashes.json +24 -0
  11. package/lib/settings-merge.js +102 -25
  12. package/lib/skills-bundle.js +2 -4
  13. package/lib/spec-choice.js +85 -0
  14. package/lib/tool-profiles.js +2 -2
  15. package/lib/uninstall.js +764 -0
  16. package/package.json +3 -3
  17. package/skywalk-sdd/index.cjs +1 -1
  18. package/skywalk-sdd/kb-upload.cjs +64 -49
  19. package/skywalk-sdd/lib/shared.cjs +62 -41
  20. package/skywalk-sdd/lib/test-execution.cjs +20 -0
  21. package/skywalk-sdd/lib/usage-reporter.cjs +17 -8
  22. package/skywalk-sdd/ontology/resolve-spec-root.cjs +49 -0
  23. package/skywalk-sdd/ontology/sdd-config.cjs +33 -5
  24. package/skywalk-sdd/ontology/spec-detect.cjs +421 -0
  25. package/skywalk-sdd/ontology/workspace-layout.cjs +46 -16
  26. package/templates/git-hooks/commit-msg +17 -99
  27. package/templates/git-hooks/commit-msg-runner.cjs +44 -0
  28. package/templates/git-hooks/commit-msg-sdd-trailer.cjs +60 -69
  29. package/templates/git-hooks/consistency-check-core.cjs +61 -240
  30. package/templates/git-hooks/delivery-check.cjs +231 -0
  31. package/templates/git-hooks/delivery-snapshot.cjs +181 -0
  32. package/templates/git-hooks/hooks.config +8 -6
  33. package/templates/git-hooks/pre-commit-consistency-check.cjs +13 -31
  34. package/templates/git-hooks/pre-push-consistency-check.cjs +36 -83
  35. package/templates/hooks/codebuddy/hooks/hook-gate-core.cjs +7 -8
  36. package/templates/openspec/proposal.md +1 -1
  37. package/templates/skills/kld-sdd/opsx-apply/SKILL.md +24 -22
  38. package/templates/skills/kld-sdd/opsx-consistency-check/SKILL.md +34 -112
  39. package/templates/skills/kld-sdd/opsx-consistency-check/scripts/scripts.cjs +15 -14
  40. package/templates/skills/kld-sdd/opsx-kb-config/SKILL.md +157 -11
  41. package/index.js +0 -13
@@ -0,0 +1,24 @@
1
+ {
2
+ "pre-commit": [
3
+ "497f012da027df5c26af3c501126e9a1c26276b038ee3b1edaaf5ab4401aa95c",
4
+ "7322d9aecafb497a5f40171a6bd7daeaadd7d2056b4a253cb0ee1a128faea36e",
5
+ "8fcdd74a3d2afaa823f9038c477c9dfd6fe53e2bccbe48cecd01535794862e4a",
6
+ "c8546c6c96adc3ecc32a15a689c59189543c2454f47998c46fbd5a0bf810cf30",
7
+ "e3b6776af51df76ff04b528152d0049a6c45d94d96a19bd2bc5ca2cc9786b22c"
8
+ ],
9
+ "pre-push": [
10
+ "405debb5e6f9186e95b727e6013b06ef3e06c598b6d2b96678d34554d3530e8c",
11
+ "461abd16663757c5a092103ac2ecae8453891c0c817d1c763369e0cdd0a8ec7b",
12
+ "80f710ff647ce8b216fb1c081fb3e22f6ee8be25847b6d02921668ba83fb1965",
13
+ "d49eb8794fde4b0d7847a876cc7083d161b290a44990ff8a036c2ebb8a00ec21",
14
+ "e6e125cbf9d7424c01b249ce03197cef9e1923ed2974a53656084488a0de60c4",
15
+ "ebf628481ae93ac0c4c6649b204449f6825291dc6b6550a71563bebec1f3ad92"
16
+ ],
17
+ "commit-msg": [
18
+ "15083d5165bff965ab2feba9a55dfedcecd54cb577c06fd11f4d1b9672d978b6",
19
+ "3b0ee15f5a126d159e14c5a3681cb7820cbf9c5e0c134ee6812053bc34330b85",
20
+ "6db7adbf17905bf14915d7e57a3c5ac46030ee3282fea5d5b211c80065406d48",
21
+ "9c5a86761b36d71b655f804a822eff675a244175799736c803e1f7379c638609",
22
+ "fc390776720393f100712fa02d5e143c2927fbb0d605a4a0242824ff7ea84574"
23
+ ]
24
+ }
@@ -1,19 +1,40 @@
1
1
  /**
2
- * Conservative merge for CodeBuddy .codebuddy/settings.json hooks.
2
+ * 编辑器 settings.json 中 SDD 受管 hook 的识别、合并与摘除。
3
+ *
4
+ * 策略归属:`.claude/settings.json` 与 `.codebuddy/settings.json` 都是**用户配置** → MERGE。
5
+ * - 安装:保留用户未知字段与自定义 hook,只剔除并重装 SDD 受管 hook(幂等)
6
+ * - 卸载:只摘除 SDD 受管 hook,文件其余内容原样保留
7
+ *
8
+ * 两个 provider 的 hook 判定集中在此模块,保证「装得进去的一定摘得掉」。
3
9
  */
4
10
 
11
+ 'use strict';
12
+
5
13
  const fs = require('fs');
6
14
  const path = require('path');
15
+ const { STRATEGY, ACTION, deployFile } = require('./deploy-strategy');
16
+
17
+ /** CodeBuddy 受管 hook:$CODEBUDDY_PROJECT_DIR/.codebuddy/hooks/sdd-*.cjs */
18
+ const SDD_CODEBUDDY_HOOK_COMMAND_RE = /(?:^|\s|["'])\$?CODEBUDDY_PROJECT_DIR[\\/]+\.codebuddy[\\/]+hooks[\\/]+sdd-[\w-]+\.cjs(?=$|\s|["'])/i;
19
+
20
+ /** Claude 受管 hook:.claude/hooks/sdd-*.cjs(含 ${CLAUDE_PROJECT_DIR} 形式) */
21
+ const SDD_CLAUDE_HOOK_COMMAND_RE = /(?:^|\s|["'])(?:\$\{CLAUDE_PROJECT_DIR\}[\\/]+)?\.claude[\\/]+hooks[\\/]+sdd-(?:prompt|pre-tool|post-tool|stop|apply-gate|apply-test-gate|skill-apply-gate)\.(?:cjs|js)(?=$|\s|["'])/i;
7
22
 
8
- const SDD_HOOK_COMMAND_RE = /(?:^|\s|["'])\$?CODEBUDDY_PROJECT_DIR[\\/]+\.codebuddy[\\/]+hooks[\\/]+sdd-[\w-]+\.cjs(?=$|\s|["'])/i;
23
+ const PROVIDERS = Object.freeze({
24
+ CLAUDE: 'claude',
25
+ CODEBUDDY: 'codebuddy',
26
+ });
9
27
 
10
28
  function hookCommandText(hook) {
11
29
  const args = Array.isArray(hook.args) ? hook.args : [];
12
30
  return [hook.command, ...args].filter(Boolean).join(' ').replace(/\s+/g, ' ').trim();
13
31
  }
14
32
 
15
- function isSddCodebuddyHook(hook) {
16
- return SDD_HOOK_COMMAND_RE.test(hookCommandText(hook));
33
+ /** 判断单个 hook 是否 SDD 受管(按 provider 选择判定规则) */
34
+ function isSddHook(hook, provider) {
35
+ const text = hookCommandText(hook);
36
+ if (provider === PROVIDERS.CLAUDE) return SDD_CLAUDE_HOOK_COMMAND_RE.test(text);
37
+ return SDD_CODEBUDDY_HOOK_COMMAND_RE.test(text);
17
38
  }
18
39
 
19
40
  function sameHookCommand(left, right) {
@@ -21,36 +42,56 @@ function sameHookCommand(left, right) {
21
42
  String(left.type || '') === String(right.type || '');
22
43
  }
23
44
 
24
- function removeManagedSddHooks(entries) {
25
- return entries
45
+ /**
46
+ * 从 hook 条目数组中剔除 SDD 受管 hook,保留用户自定义条目。
47
+ * 条目内 hooks 被清空时整条移除。
48
+ */
49
+ function removeManagedHooks(entries, provider) {
50
+ return (entries || [])
26
51
  .map(entry => {
27
52
  if (!Array.isArray(entry.hooks)) {
28
53
  return entry;
29
54
  }
30
55
  return {
31
56
  ...entry,
32
- hooks: entry.hooks.filter(hook => !isSddCodebuddyHook(hook)),
57
+ hooks: entry.hooks.filter(hook => !isSddHook(hook, provider)),
33
58
  };
34
59
  })
35
60
  .filter(entry => !Array.isArray(entry.hooks) || entry.hooks.length > 0);
36
61
  }
37
62
 
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');
63
+ /**
64
+ * 纯计算:摘除 settings 对象中全部 SDD 受管 hook(卸载路径)。
65
+ * 返回新对象,不修改入参;hooks 变空时保留空对象结构,避免下游读取报错。
66
+ */
67
+ function stripManagedHooks(settings, provider) {
68
+ const next = { ...settings };
69
+ if (!next.hooks || typeof next.hooks !== 'object') {
70
+ return next;
71
+ }
72
+ const hooks = {};
73
+ for (const [eventName, entries] of Object.entries(next.hooks)) {
74
+ hooks[eventName] = removeManagedHooks(entries, provider);
43
75
  }
76
+ next.hooks = hooks;
77
+ return next;
78
+ }
44
79
 
80
+ /**
81
+ * 纯计算:把模板 hooks 合并进已有配置,保留用户未知字段与自定义 hook。
82
+ *
83
+ * @param {string|null} existingRaw 现有 settings.json 原文(不存在传 null)
84
+ * @param {object} templateSettings 模板 settings 对象
85
+ * @param {string} [provider] 受管 hook 归属,默认 codebuddy
86
+ * @returns {{ok: true, merged: object} | {ok: false, error: string}}
87
+ */
88
+ function computeMergedSettings(existingRaw, templateSettings, provider = PROVIDERS.CODEBUDDY) {
45
89
  let existingSettings = {};
46
- if (originalBytes) {
90
+ if (existingRaw) {
47
91
  try {
48
- existingSettings = JSON.parse(originalBytes);
92
+ existingSettings = JSON.parse(existingRaw);
49
93
  } catch {
50
- return {
51
- ok: false,
52
- error: `${displayPath} 不是合法 JSON,请修复后重试(路径: ${settingsPath})`,
53
- };
94
+ return { ok: false, error: '不是合法 JSON' };
54
95
  }
55
96
  }
56
97
 
@@ -58,7 +99,7 @@ function mergeCodebuddySettings(settingsPath, templateSettings) {
58
99
  merged.hooks = merged.hooks || {};
59
100
 
60
101
  for (const [eventName, entries] of Object.entries(templateSettings.hooks || {})) {
61
- merged.hooks[eventName] = removeManagedSddHooks(merged.hooks[eventName] || []);
102
+ merged.hooks[eventName] = removeManagedHooks(merged.hooks[eventName], provider);
62
103
  for (const entry of entries) {
63
104
  const templateHooks = entry.hooks || [];
64
105
  const exists = merged.hooks[eventName].some(existingEntry => {
@@ -73,13 +114,49 @@ function mergeCodebuddySettings(settingsPath, templateSettings) {
73
114
  }
74
115
  }
75
116
 
76
- fs.writeFileSync(settingsPath, JSON.stringify(merged, null, 2) + '\n', 'utf8');
77
- return { ok: true };
117
+ return { ok: true, merged };
118
+ }
119
+
120
+ /**
121
+ * 从模板文件部署 settings.json(用户配置 → MERGE)。
122
+ * 所有写盘经 deploy-strategy,非法 JSON 时 fail-closed 并保持原字节不变。
123
+ *
124
+ * @returns {{ok: boolean, action?: string, error?: string}}
125
+ */
126
+ function mergeCodebuddySettingsFromTemplate(sourcePath, targetPath) {
127
+ const displayPath = path.basename(targetPath);
128
+ let templateSettings;
129
+ try {
130
+ templateSettings = JSON.parse(fs.readFileSync(sourcePath, 'utf8'));
131
+ } catch (error) {
132
+ return { ok: false, error: `模板 ${displayPath} 无法解析: ${error.message}` };
133
+ }
134
+
135
+ const result = deployFile(sourcePath, targetPath, {
136
+ strategy: STRATEGY.MERGE,
137
+ label: displayPath,
138
+ mergeFn: ({ readTarget }) => {
139
+ const computed = computeMergedSettings(readTarget(), templateSettings, PROVIDERS.CODEBUDDY);
140
+ if (!computed.ok) {
141
+ return {
142
+ ok: false,
143
+ error: `${displayPath} 不是合法 JSON,请修复后重试(路径: ${targetPath})`,
144
+ };
145
+ }
146
+ fs.writeFileSync(targetPath, JSON.stringify(computed.merged, null, 2) + '\n', 'utf8');
147
+ return { ok: true, action: ACTION.MERGED };
148
+ },
149
+ });
150
+
151
+ if (!result.ok) {
152
+ return { ok: false, error: result.message };
153
+ }
154
+ return { ok: true, action: result.action };
78
155
  }
79
156
 
80
157
  module.exports = {
81
- mergeCodebuddySettings,
82
- isSddCodebuddyHook,
83
- hookCommandText,
84
- removeManagedSddHooks,
158
+ PROVIDERS,
159
+ mergeCodebuddySettingsFromTemplate,
160
+ computeMergedSettings,
161
+ stripManagedHooks,
85
162
  };
@@ -2,6 +2,8 @@
2
2
  * SDD skills bundle 共享配置与模板渲染
3
3
  */
4
4
 
5
+ 'use strict';
6
+
5
7
  const fs = require('fs');
6
8
  const path = require('path');
7
9
 
@@ -186,10 +188,6 @@ module.exports = {
186
188
  OPSX_SKILL_METADATA,
187
189
  buildOpsxSkillMetadata,
188
190
  commandNameFromSkillDir,
189
- displayNameFromCommandName,
190
- parseSkillFrontmatter,
191
191
  renderTemplate,
192
- replaceProfileTokens,
193
- transformKunlunSlashCommands,
194
192
  validateRenderedContent,
195
193
  };
@@ -0,0 +1,85 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * spec 候选的单选交互 —— init / uninstall / sync-repos 三个场景**共用**。
5
+ *
6
+ * 为什么需要用户裁决:判据无法区分「当前项目的 spec 仓」与「用户手动复制的
7
+ * 备份」(如 `erp-sdd-specs copy`)—— 二者都命中。故由用户单选,而不是静默
8
+ * 挑一个:挑错的代价是 init 把内容灌进错误目录、uninstall 删错仓、
9
+ * sync-repos 把错误路径写进每个代码仓的 `git config`。
10
+ *
11
+ * 归属 lib/ 而非 bin/:init 的交互发生在 `lib/init.js#main()` 内部,uninstall
12
+ * 与 sync-repos 的交互在 bin/ 层。放在 lib/ 使两侧都能复用,且依赖方向正确
13
+ *(bin → lib,不反向)。
14
+ *
15
+ * 交互约定(三个场景一致):
16
+ * - 0 个候选 → 返回 undefined(调用方决定「要求输入路径」还是「报错退出」)
17
+ * - 1 个候选 → 确认(回车默认 Y)
18
+ * - N 个候选 → 列出(标注来源)让用户单选
19
+ *
20
+ * 实现约束:候选信息必须**并入 question 字符串**,不能靠提问前的 console.log。
21
+ * readline 重绘提示行时会发 \x1b[0J(清除光标之后内容),会把前置输出抹掉,
22
+ * 导致用户看不到自己在确认哪个目录。
23
+ */
24
+
25
+ const { specSourceLabel } = require('../skywalk-sdd/ontology/spec-detect.cjs');
26
+
27
+ /**
28
+ * 让用户在 spec 候选集合中单选。
29
+ *
30
+ * @param {{ask: (q: string) => Promise<string>}} prompt readline 会话(只需 ask)
31
+ * @param {Array<{abs: string, name: string, source: string, tier?: number}>} candidates
32
+ * 候选集合(由 spec-detect 枚举,已按 tier 排序)
33
+ * @param {{actionLabel?: string}} [options]
34
+ * actionLabel:提问文案里的动作词(默认「卸载」;init 用「使用」、
35
+ * sync-repos 用「接入」)
36
+ * @returns {Promise<string|null|undefined>}
37
+ * string=选中的绝对路径;null=用户拒绝/取消;undefined=无候选
38
+ */
39
+ async function selectSpecRoot(prompt, candidates, options = {}) {
40
+ if (!candidates || candidates.length === 0) return undefined;
41
+ const action = options.actionLabel || '卸载';
42
+
43
+ if (candidates.length === 1) {
44
+ const only = candidates[0];
45
+ const question = [
46
+ `\n🔍 检测到 spec 仓库: ${only.name} [${specSourceLabel(only.source)}]`,
47
+ ` 位置: ${only.abs}`,
48
+ `是否${action}该 spec 仓库?(Y=是 / n=重新选择,默认 Y): `,
49
+ ].join('\n');
50
+ while (true) {
51
+ const answer = await prompt.ask(question);
52
+ const normalized = String(answer || '').trim().toLowerCase();
53
+ if (!normalized || normalized === 'y' || normalized === 'yes') return only.abs;
54
+ if (normalized === 'n' || normalized === 'no') break;
55
+ console.log(`❌ 无效输入 "${answer}",请输入 Y 或 n`);
56
+ }
57
+ // 拒绝唯一候选 → 无其它候选可选,交由调用方决定后续(重新选择 / 输入路径)
58
+ return null;
59
+ }
60
+
61
+ while (true) {
62
+ const question = [
63
+ `\n检测到多个 spec 仓库,请选择要${action}的一个:`,
64
+ ...candidates.map(
65
+ (c, i) => ` ${i + 1}. ${c.name} [${specSourceLabel(c.source)}]\n ${c.abs}`,
66
+ ),
67
+ ' 0. 取消',
68
+ `\n请输入选项 (0-${candidates.length}): `,
69
+ ].join('\n');
70
+
71
+ const answer = await prompt.ask(question);
72
+ const trimmed = String(answer || '').trim();
73
+ if (trimmed === '0' || /^(q|quit|exit)$/i.test(trimmed)) return null;
74
+
75
+ const index = Number(trimmed);
76
+ if (Number.isInteger(index) && index >= 1 && index <= candidates.length) {
77
+ const chosen = candidates[index - 1];
78
+ console.log(`✓ 已选择: ${chosen.name}\n`);
79
+ return chosen.abs;
80
+ }
81
+ console.log(`❌ 无效选项 "${answer}",请输入 0-${candidates.length} 之间的数字\n`);
82
+ }
83
+ }
84
+
85
+ module.exports = { selectSpecRoot };
@@ -3,6 +3,8 @@
3
3
  * Single source of truth for per-agent paths and capabilities.
4
4
  */
5
5
 
6
+ 'use strict';
7
+
6
8
  const path = require('path');
7
9
 
8
10
  const TOOL_PROFILES = Object.freeze({
@@ -272,7 +274,5 @@ module.exports = {
272
274
  getToolProfile,
273
275
  listToolProfileIds,
274
276
  validateToolProfiles,
275
- isSafeRelativePath,
276
277
  buildToolConfigs,
277
- DEFAULT_TOOL_DISPLAY_NAMES,
278
278
  };