kld-sdd 2.6.10 → 2.6.12

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 (27) hide show
  1. package/lib/hook-gate-core.js +41 -5
  2. package/lib/init.js +154 -31
  3. package/package.json +1 -1
  4. package/skywalk-sdd/ontology/naming-diagnose.cjs +26 -3
  5. package/templates/git-hooks/commit-msg-sdd-trailer.cjs +11 -1
  6. package/templates/hooks/claude/hooks/sdd-prompt.cjs +23 -5
  7. package/templates/hooks/codebuddy/hooks/sdd-prompt.cjs +22 -3
  8. package/templates/skills/kld-sdd/opsx-apply/SKILL.md +3 -3
  9. package/templates/skills/kld-sdd/opsx-apply/checklist.md +3 -3
  10. package/templates/skills/kld-sdd/opsx-apply/reference.md +13 -13
  11. package/templates/skills/kld-sdd/opsx-apply/worktree-setup.md +3 -3
  12. package/templates/skills/kld-sdd/opsx-archive/SKILL.md +14 -14
  13. package/templates/skills/kld-sdd/opsx-archive/checklist.md +2 -2
  14. package/templates/skills/kld-sdd/opsx-check/SKILL.md +13 -13
  15. package/templates/skills/kld-sdd/opsx-design/SKILL.md +4 -4
  16. package/templates/skills/kld-sdd/opsx-design/reference.md +2 -2
  17. package/templates/skills/kld-sdd/opsx-explore/SKILL.md +5 -5
  18. package/templates/skills/kld-sdd/opsx-propose/SKILL.md +11 -11
  19. package/templates/skills/kld-sdd/opsx-propose/reference.md +2 -2
  20. package/templates/skills/kld-sdd/opsx-rules/SKILL.md +3 -3
  21. package/templates/skills/kld-sdd/opsx-spec/SKILL.md +5 -5
  22. package/templates/skills/kld-sdd/opsx-spec/reference.md +2 -2
  23. package/templates/skills/kld-sdd/opsx-task/SKILL.md +4 -4
  24. package/templates/skills/kld-sdd/opsx-task/reference.md +2 -2
  25. package/templates/skills/kld-sdd/opsx-tdd-core/reference.md +3 -3
  26. package/templates/skills/kld-sdd/opsx-tdd-rules/rules/test-skeleton-telemetry.md +2 -2
  27. package/templates/skills/kld-sdd/opsx-test/SKILL.md +5 -5
@@ -50,16 +50,45 @@ function normalizeHookInput(input) {
50
50
  return normalized;
51
51
  }
52
52
 
53
+ function resolveSkywalkDir(dir) {
54
+ const root = path.resolve(dir || '.');
55
+ const direct = path.join(root, 'skywalk-sdd');
56
+ if (
57
+ fs.existsSync(path.join(direct, 'log.cjs'))
58
+ || fs.existsSync(path.join(direct, 'log.js'))
59
+ ) {
60
+ return direct;
61
+ }
62
+ const hintFile = path.join(root, '.sdd-spec-root');
63
+ if (fs.existsSync(hintFile)) {
64
+ const rel = String(fs.readFileSync(hintFile, 'utf8') || '')
65
+ .trim()
66
+ .split(/\r?\n/)[0]
67
+ .trim();
68
+ if (rel) {
69
+ const nested = path.join(root, rel, 'skywalk-sdd');
70
+ if (
71
+ fs.existsSync(path.join(nested, 'log.cjs'))
72
+ || fs.existsSync(path.join(nested, 'log.js'))
73
+ ) {
74
+ return nested;
75
+ }
76
+ }
77
+ }
78
+ return null;
79
+ }
80
+
53
81
  function hasTelemetryCli(dir) {
54
- return fs.existsSync(path.join(dir, 'skywalk-sdd', 'log.cjs')) ||
55
- fs.existsSync(path.join(dir, 'skywalk-sdd', 'log.js'));
82
+ return Boolean(resolveSkywalkDir(dir));
56
83
  }
57
84
 
58
85
  function findProjectRoot(startDir) {
59
86
  if (!startDir) return '';
60
87
  let current = path.resolve(startDir);
61
88
  for (let i = 0; i < 25; i++) {
62
- if (hasTelemetryCli(current)) return current;
89
+ if (hasTelemetryCli(current) || fs.existsSync(path.join(current, '.sdd-spec-root'))) {
90
+ return current;
91
+ }
63
92
  const parent = path.dirname(current);
64
93
  if (parent === current) return '';
65
94
  current = parent;
@@ -96,7 +125,10 @@ function safeChangeName(name) {
96
125
  }
97
126
 
98
127
  function findActiveApplyStage(projectRoot) {
99
- const stateDir = path.join(projectRoot, 'skywalk-sdd', 'state');
128
+ const skywalk = resolveSkywalkDir(projectRoot);
129
+ const stateDir = skywalk
130
+ ? path.join(skywalk, 'state')
131
+ : path.join(projectRoot, 'skywalk-sdd', 'state');
100
132
  if (!fs.existsSync(stateDir)) return null;
101
133
 
102
134
  let latest = null;
@@ -118,7 +150,10 @@ function findActiveApplyStage(projectRoot) {
118
150
 
119
151
  function hasCompletedCheck(projectRoot, changeName) {
120
152
  const safeName = safeChangeName(changeName);
121
- const eventsChangeDir = path.join(projectRoot, 'skywalk-sdd', 'events', safeName);
153
+ const skywalk = resolveSkywalkDir(projectRoot);
154
+ const eventsChangeDir = skywalk
155
+ ? path.join(skywalk, 'events', safeName)
156
+ : path.join(projectRoot, 'skywalk-sdd', 'events', safeName);
122
157
  if (!fs.existsSync(eventsChangeDir)) return false;
123
158
 
124
159
  const jsonlFiles = fs.readdirSync(eventsChangeDir)
@@ -316,6 +351,7 @@ module.exports = {
316
351
  readStdin,
317
352
  parseHookInput,
318
353
  normalizeHookInput,
354
+ resolveSkywalkDir,
319
355
  hasTelemetryCli,
320
356
  findProjectRoot,
321
357
  getProjectRoot,
package/lib/init.js CHANGED
@@ -862,7 +862,7 @@ function deployTelemetryDataDir(targetCwd = process.cwd()) {
862
862
  return true;
863
863
  }
864
864
 
865
- function installGitHookShim(cwd, hookName, scriptPath) {
865
+ function installGitHookShim(cwd, hookName, scriptPath, options = {}) {
866
866
  const hooksDir = path.join(cwd, '.git', 'hooks');
867
867
  if (!fs.existsSync(hooksDir)) {
868
868
  return;
@@ -870,11 +870,20 @@ function installGitHookShim(cwd, hookName, scriptPath) {
870
870
 
871
871
  const hookPath = path.join(hooksDir, hookName);
872
872
  const marker = 'KLD SDD quality gate';
873
+ // Git 把 commit-msg 文件路径放在 $1;--project 等额外参数必须跟在 "$@" 后面,
874
+ // 否则 trailer 脚本会把 --project=… 误当成 message 文件(argv[2])。
875
+ const extraArgs = options.extraArgs || '';
876
+ const nodeLine = extraArgs
877
+ ? ` node "${scriptPath}" "$@" ${extraArgs}`
878
+ : ` node "${scriptPath}" "$@"`;
873
879
  const shim = [
874
880
  '#!/bin/sh',
875
881
  `# ${marker}`,
876
882
  `if [ -f "${scriptPath}" ]; then`,
877
- ` node "${scriptPath}" "$@"`,
883
+ nodeLine,
884
+ 'else',
885
+ ` echo "[kld-sdd] Hook 脚本不存在: ${scriptPath}" >&2`,
886
+ ' exit 1',
878
887
  'fi',
879
888
  '',
880
889
  ].join('\n');
@@ -882,10 +891,21 @@ function installGitHookShim(cwd, hookName, scriptPath) {
882
891
  if (fs.existsSync(hookPath)) {
883
892
  const existing = fs.readFileSync(hookPath, 'utf8');
884
893
  if (existing.includes(marker)) {
885
- console.log(` ✓ .git/hooks/${hookName} 已包含 SDD 质量门禁`);
886
- } else {
887
- console.log(` ℹ️ .git/hooks/${hookName} 已存在,已保留不覆盖;可手动调用 ${scriptPath}`);
894
+ // 已是 SDD shim:若指向路径变化(如改为引用 spec 仓),允许覆盖升级
895
+ if (existing.includes(scriptPath) && (!extraArgs || existing.includes(extraArgs.trim()))) {
896
+ console.log(` .git/hooks/${hookName} 已包含 SDD 质量门禁`);
897
+ return;
898
+ }
899
+ fs.writeFileSync(hookPath, shim, 'utf8');
900
+ try {
901
+ fs.chmodSync(hookPath, 0o755);
902
+ } catch {
903
+ // ignore
904
+ }
905
+ console.log(` ✓ 更新 .git/hooks/${hookName} SDD 质量门禁`);
906
+ return;
888
907
  }
908
+ console.log(` ℹ️ .git/hooks/${hookName} 已存在,已保留不覆盖;可手动调用 ${scriptPath}`);
889
909
  return;
890
910
  }
891
911
 
@@ -898,14 +918,52 @@ function installGitHookShim(cwd, hookName, scriptPath) {
898
918
  console.log(` ✓ 安装 .git/hooks/${hookName} SDD 质量门禁`);
899
919
  }
900
920
 
921
+ /**
922
+ * 确保 spec 包裹包内有 commit-msg 脚本(供代码仓 Hook 远程引用)。
923
+ */
924
+ function ensureSpecCommitMsgScript(specRepoRoot, pkgPath = getPackagePath()) {
925
+ const specRoot = path.resolve(specRepoRoot);
926
+ const scriptPath = path.join(specRoot, 'skywalk-sdd', 'git-hooks', 'commit-msg-sdd-trailer.cjs');
927
+ if (fs.existsSync(scriptPath)) return { ok: true, scriptPath };
928
+
929
+ const src = path.join(pkgPath, 'templates', 'git-hooks', 'commit-msg-sdd-trailer.cjs');
930
+ if (!fs.existsSync(src)) {
931
+ return { ok: false, message: `commit-msg 模板缺失: ${src}` };
932
+ }
933
+ const ontologySrc = path.join(pkgPath, 'skywalk-sdd', 'ontology');
934
+ const ontologyDst = path.join(specRoot, 'skywalk-sdd', 'ontology');
935
+ if (fs.existsSync(ontologySrc) && !fs.existsSync(path.join(ontologyDst, 'change-key.cjs'))) {
936
+ copyDir(ontologySrc, ontologyDst);
937
+ }
938
+ fs.mkdirSync(path.dirname(scriptPath), { recursive: true });
939
+ fs.copyFileSync(src, scriptPath);
940
+ return { ok: true, scriptPath, deployed: true };
941
+ }
942
+
943
+ /**
944
+ * 代码仓 commit-msg:只写 .git/hooks,脚本与 ontology 一律用 spec 仓那一份。
945
+ */
946
+ function installCodeRepoCommitMsgHook(codeRepoRoot, specRepoRoot, options = {}) {
947
+ const cwd = path.resolve(codeRepoRoot);
948
+ const ensured = ensureSpecCommitMsgScript(specRepoRoot, options.pkgPath || getPackagePath());
949
+ if (!ensured.ok) {
950
+ console.log(` ⚠️ ${ensured.message}`);
951
+ return false;
952
+ }
953
+ installGitHookShim(cwd, 'commit-msg', ensured.scriptPath, {
954
+ extraArgs: `--project=${cwd}`,
955
+ });
956
+ return true;
957
+ }
958
+
901
959
  /**
902
960
  * 部署 Git hooks / CI 兜底模板
903
961
  * 这些脚本只做质量门禁和 CI 补充记录,不替代 OPSX 主采集链路。
904
962
  *
905
963
  * @param {string} [targetCwd]
906
964
  * @param {{ mode?: 'full'|'commit-msg-only' }} [options]
907
- * full: spec 仓(pre-commit/pre-push/commit-msg + CI)
908
- * commit-msg-only: 代码仓(只装 Trailer Hook,不装 skills、不装文档门禁)
965
+ * full: spec 包裹包(pre-commit/pre-push/commit-msg + CI)
966
+ * commit-msg-only: 单仓 Git 根(本仓落 skywalk-sdd/git-hooks;多仓代码仓请用 installCodeRepoCommitMsgHook)
909
967
  */
910
968
  function deployQualityGateTemplates(targetCwd = process.cwd(), options = {}) {
911
969
  const pkgPath = getPackagePath();
@@ -946,12 +1004,12 @@ function deployQualityGateTemplates(targetCwd = process.cwd(), options = {}) {
946
1004
  }
947
1005
 
948
1006
  /**
949
- * 代码仓轻量接入:只部署 Hook 运行所需 ontology + commit-msg,不部署 skills
1007
+ * 代码仓轻量接入:只写 .sdd.yaml + sdd.specPath + .git/hooks/commit-msg。
1008
+ * 不在代码仓落 skywalk-sdd/——Hook 脚本与 ontology 引用 spec 包裹包那一份。
950
1009
  */
951
1010
  function attachCodeRepoLite(codeRepoRoot, specRepoRoot, options = {}) {
952
1011
  const cwd = path.resolve(codeRepoRoot);
953
1012
  const specRoot = path.resolve(specRepoRoot);
954
- const pkgPath = options.pkgPath || getPackagePath();
955
1013
  const label = path.basename(cwd);
956
1014
 
957
1015
  if (!workspaceLayout.isGitRepo(cwd)) {
@@ -961,16 +1019,7 @@ function attachCodeRepoLite(codeRepoRoot, specRepoRoot, options = {}) {
961
1019
  return { ok: false, repo: label, message: `spec 路径不是 Git 仓库: ${specRoot}` };
962
1020
  }
963
1021
 
964
- console.log(`🔗 轻量接入代码仓(无 skills): ${label}`);
965
-
966
- // Hook 依赖 ontology + git-hooks;不部署完整 telemetry/skills
967
- const dataDir = path.join(cwd, 'skywalk-sdd');
968
- const ontologySrc = path.join(pkgPath, 'skywalk-sdd', 'ontology');
969
- const ontologyDst = path.join(dataDir, 'ontology');
970
- if (fs.existsSync(ontologySrc)) {
971
- copyDir(ontologySrc, ontologyDst);
972
- }
973
- deployQualityGateTemplates(cwd, { mode: 'commit-msg-only' });
1022
+ console.log(`🔗 轻量接入代码仓(无 skywalk-sdd/、无 skills): ${label}`);
974
1023
 
975
1024
  const remote = sddConfig.gitRemoteUrl(specRoot) || 'git@gitlab.example.com:biz/xxx-sdd-specs.git';
976
1025
  const sddYamlPath = path.join(cwd, '.sdd.yaml');
@@ -996,6 +1045,9 @@ function attachCodeRepoLite(codeRepoRoot, specRepoRoot, options = {}) {
996
1045
  }
997
1046
  console.log(` ✓ sdd.specPath = ${linked.path}`);
998
1047
 
1048
+ installCodeRepoCommitMsgHook(cwd, specRoot, options);
1049
+ console.log(' ✓ commit-msg → spec 仓 skywalk-sdd/git-hooks/(代码仓不落 skywalk-sdd/)');
1050
+
999
1051
  return { ok: true, repo: label, path: cwd, specPath: linked.path };
1000
1052
  }
1001
1053
 
@@ -1155,6 +1207,66 @@ function writeSddSpecRootHint(anchorDir, specPackageAbs) {
1155
1207
  return filePath;
1156
1208
  }
1157
1209
 
1210
+ /**
1211
+ * 移除工作区 / Git 根上多余的 skywalk-sdd(符号链接或旧完整副本)。
1212
+ * 唯一实体只保留在 *-sdd-specs/skywalk-sdd/。
1213
+ *
1214
+ * 触发条件(满足其一即可):
1215
+ * - 存在 .sdd-spec-root
1216
+ * - 同级已有 *-sdd-specs/skywalk-sdd(多仓/单仓包裹包)
1217
+ */
1218
+ function removeOuterSkywalk(anchorDir) {
1219
+ const anchor = path.resolve(anchorDir);
1220
+ const outer = path.join(anchor, 'skywalk-sdd');
1221
+ let st = null;
1222
+ try {
1223
+ st = fs.lstatSync(outer);
1224
+ } catch (err) {
1225
+ if (err && err.code === 'ENOENT') return { ok: true, removed: false };
1226
+ throw err;
1227
+ }
1228
+
1229
+ const hint = path.join(anchor, '.sdd-spec-root');
1230
+ const hasHint = fs.existsSync(hint);
1231
+ let hasNestedEntity = false;
1232
+ try {
1233
+ for (const name of fs.readdirSync(anchor)) {
1234
+ if (!/-sdd-specs$/i.test(name)) continue;
1235
+ const entity = path.join(anchor, name, 'skywalk-sdd', 'log.cjs');
1236
+ if (fs.existsSync(entity)) {
1237
+ hasNestedEntity = true;
1238
+ break;
1239
+ }
1240
+ }
1241
+ } catch {
1242
+ // ignore
1243
+ }
1244
+
1245
+ if (!hasHint && !hasNestedEntity) {
1246
+ // 无包裹包迹象时不贸然删除(可能是旧平铺布局)
1247
+ return { ok: true, removed: false, skipped: true };
1248
+ }
1249
+
1250
+ try {
1251
+ if (st.isSymbolicLink()) {
1252
+ fs.unlinkSync(outer);
1253
+ console.log(' ✓ 已移除 Git/工作区根多余的 skywalk-sdd 符号链接(实体在包裹包)');
1254
+ return { ok: true, removed: true, mode: 'symlink' };
1255
+ }
1256
+ if (st.isDirectory()) {
1257
+ fs.rmSync(outer, { recursive: true, force: true });
1258
+ console.log(' ✓ 已移除 Git/工作区根多余的 skywalk-sdd/(实体在包裹包)');
1259
+ return { ok: true, removed: true, mode: 'directory' };
1260
+ }
1261
+ fs.unlinkSync(outer);
1262
+ console.log(' ✓ 已移除 Git/工作区根多余的 skywalk-sdd');
1263
+ return { ok: true, removed: true, mode: 'file' };
1264
+ } catch (err) {
1265
+ console.log(` ⚠️ 未能移除根目录 skywalk-sdd/: ${err.message}`);
1266
+ return { ok: false, message: err.message };
1267
+ }
1268
+ }
1269
+
1158
1270
  /**
1159
1271
  * 检测当前项目已初始化的 AI 编辑器
1160
1272
  */
@@ -1185,6 +1297,8 @@ function updateGitignore() {
1185
1297
  '.agents/commands/personal-*',
1186
1298
  'skywalk-sdd/events/',
1187
1299
  'skywalk-sdd/state/',
1300
+ '**/skywalk-sdd/events/',
1301
+ '**/skywalk-sdd/state/',
1188
1302
  '.worktrees/'
1189
1303
  ];
1190
1304
 
@@ -1198,9 +1312,11 @@ function updateGitignore() {
1198
1312
  .workbuddy/commands/personal-*
1199
1313
  .agents/commands/personal-*
1200
1314
 
1201
- # SDD Telemetry 数据(本地度量,可选提交;本体 JSON 与报告随 openspec/changes/<change>/ 提交)
1315
+ # SDD Telemetry 数据(实体在 *-sdd-specs/skywalk-sdd/;工作区可能是 symlink)
1202
1316
  skywalk-sdd/events/
1203
1317
  skywalk-sdd/state/
1318
+ **/skywalk-sdd/events/
1319
+ **/skywalk-sdd/state/
1204
1320
 
1205
1321
  # SDD Apply 隔离 worktree(本地临时目录)
1206
1322
  .worktrees/
@@ -1360,8 +1476,6 @@ async function main() {
1360
1476
  deployProfileArtifacts(selectedTools, workspaceRoot, getPackagePath());
1361
1477
 
1362
1478
  if (layout.isWorkspace) {
1363
- deployTelemetryDataDir(workspaceRoot);
1364
-
1365
1479
  if (!specPackage) {
1366
1480
  console.log('📦 未发现 spec 仓,正在创建统一 SDD 包裹包(独立 Git)...');
1367
1481
  specPackage = workspaceLayout.ensureSpecPackage(workspaceRoot, { initGit: true });
@@ -1373,6 +1487,7 @@ async function main() {
1373
1487
  asIndependentGit: true,
1374
1488
  });
1375
1489
  writeSddSpecRootHint(workspaceRoot, specPackage.abs);
1490
+ removeOuterSkywalk(workspaceRoot);
1376
1491
 
1377
1492
  // 重新探测,刷新 layout.specRepo / codeRepos
1378
1493
  layout = workspaceLayout.detectWorkspaceLayout(workspaceRoot);
@@ -1390,7 +1505,6 @@ async function main() {
1390
1505
  } else {
1391
1506
  // 单仓:Git 根只放代码 + skills + Hook;SDD 内容进统一包裹子目录
1392
1507
  const isGit = workspaceLayout.isGitRepo(workspaceRoot);
1393
- deployTelemetryDataDir(workspaceRoot);
1394
1508
 
1395
1509
  if (isGit) {
1396
1510
  console.log('📦 单仓模式:创建/复用仓内统一 SDD 包裹目录...');
@@ -1402,10 +1516,11 @@ async function main() {
1402
1516
  asIndependentGit: false,
1403
1517
  });
1404
1518
  writeSddSpecRootHint(workspaceRoot, specPackage.abs);
1519
+ removeOuterSkywalk(workspaceRoot);
1405
1520
 
1406
1521
  deployMonoSddYaml(workspaceRoot, { specPathRelative: specPackage.name });
1407
- // Hook 装在 Git 根;Trailer 读包裹目录里的 sdd.config / openspec
1408
- deployQualityGateTemplates(workspaceRoot, { mode: 'commit-msg-only' });
1522
+ // Hook 装在 Git 根,脚本在包裹包内(Git 根不落 skywalk-sdd/)
1523
+ installCodeRepoCommitMsgHook(workspaceRoot, specPackage.abs);
1409
1524
  const linked = sddConfig.setSpecPath(workspaceRoot, specPackage.abs, {
1410
1525
  allowNestedPackage: true,
1411
1526
  });
@@ -1422,6 +1537,8 @@ async function main() {
1422
1537
  skipOpenspec,
1423
1538
  asIndependentGit: false,
1424
1539
  });
1540
+ writeSddSpecRootHint(workspaceRoot, specPackage.abs);
1541
+ removeOuterSkywalk(workspaceRoot);
1425
1542
  deployDotSddYaml(workspaceRoot);
1426
1543
  }
1427
1544
  }
@@ -1453,6 +1570,12 @@ async function main() {
1453
1570
  // 7. 更新工作目录 .gitignore
1454
1571
  updateGitignore();
1455
1572
 
1573
+ // 8. 收尾:无论前面是否走过 symlink/旧副本路径,强制清掉工作区/Git 根多余 skywalk-sdd/
1574
+ if (specPackage && fs.existsSync(specPackage.abs)) {
1575
+ writeSddSpecRootHint(workspaceRoot, specPackage.abs);
1576
+ }
1577
+ removeOuterSkywalk(workspaceRoot);
1578
+
1456
1579
  const selectedNames = selectedTools.map(t => TOOL_CONFIGS[t].name).join(', ');
1457
1580
 
1458
1581
  console.log();
@@ -1466,12 +1589,14 @@ async function main() {
1466
1589
  console.log('工作区部署结果:');
1467
1590
  console.log(' 🎯 工作目录 .*/skills/opsx-*/ # skills 只装这一份');
1468
1591
  console.log(' 📋 .sdd-workspace.yaml # 子仓编排清单');
1592
+ console.log(' 📌 .sdd-spec-root # 指向包裹包;CLI 用 $(cat .sdd-spec-root)/skywalk-sdd/…');
1593
+ console.log(' ✗ 工作区根无 skywalk-sdd/ # 实体只在包裹包内');
1469
1594
  if (specPackage) {
1470
1595
  console.log(` 📄 ${specPackage.name}/ # 统一 SDD 包裹包(独立 Git)`);
1471
1596
  console.log(' modules.yaml / sdd.config.yaml / openspec / openspec-templates / skywalk-sdd');
1472
1597
  }
1473
1598
  for (const repo of layout.codeRepos) {
1474
- console.log(` 🔗 ${repo.name}/ ← commit-msg Hook + .sdd.yaml(无 skills)`);
1599
+ console.log(` 🔗 ${repo.name}/ ← commit-msg Hook + .sdd.yaml(无 skywalk-sdd/、无 skills)`);
1475
1600
  }
1476
1601
  console.log();
1477
1602
  console.log('新代码仓加入后执行:');
@@ -1484,7 +1609,8 @@ async function main() {
1484
1609
  console.log(` 📄 ${specPackage.name}/ # 统一 SDD 包裹包(仓内子目录)`);
1485
1610
  console.log(' modules.yaml / sdd.config.yaml / openspec / openspec-templates / skywalk-sdd');
1486
1611
  }
1487
- console.log(' 🔗 .sdd.yaml (layout: mono) + commit-msg Hook');
1612
+ console.log(' 🔗 .sdd.yaml (layout: mono) + commit-msg Hook(脚本在包裹包)');
1613
+ console.log(' 📌 .sdd-spec-root → 包裹包;无根目录 skywalk-sdd/');
1488
1614
  }
1489
1615
  if (selectedTools.includes('kunlunzhima')) {
1490
1616
  console.log(' 📎 .kunlunzhima/commands/opsx/ # KunlunZhima OPSX command bridge(11 个)');
@@ -1496,10 +1622,6 @@ async function main() {
1496
1622
  if (selectedTools.includes('claude')) {
1497
1623
  console.log(' 🪝 .claude/hooks/ # Claude Code SDD Hook Pack(可选增强)');
1498
1624
  }
1499
- if (!layout.isWorkspace) {
1500
- console.log(' 📊 skywalk-sdd/ # Hook 运行时 ontology(Git 根)');
1501
- console.log(' 🧰 skywalk-sdd/git-hooks/ # commit-msg Trailer Hook');
1502
- }
1503
1625
  console.log();
1504
1626
 
1505
1627
  // 统一的 skill 格式说明
@@ -1564,6 +1686,7 @@ module.exports = {
1564
1686
  deployMonoSddYaml,
1565
1687
  populateSpecPackage,
1566
1688
  writeSddSpecRootHint,
1689
+ removeOuterSkywalk,
1567
1690
  deployQualityGateTemplates,
1568
1691
  deployTelemetryDataDir,
1569
1692
  attachCodeRepoLite,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kld-sdd",
3
- "version": "2.6.10",
3
+ "version": "2.6.12",
4
4
  "description": "KLD SDD OpenSpec 项目初始化工具 - 一键部署 SDD skills",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -67,17 +67,40 @@ function detectEntryMode(projectRoot) {
67
67
  return 'unknown';
68
68
  }
69
69
 
70
+ function resolveCommitMsgScriptPath(projectRoot) {
71
+ const localScript = path.join(projectRoot, 'skywalk-sdd', 'git-hooks', 'commit-msg-sdd-trailer.cjs');
72
+ if (fs.existsSync(localScript)) return localScript;
73
+ // 多仓代码仓:脚本在 spec 包裹包,不在本仓 skywalk-sdd/
74
+ try {
75
+ const context = sddConfig.resolveCodeRepoContext(projectRoot);
76
+ if (context && context.specPath) {
77
+ const remote = path.join(context.specPath, 'skywalk-sdd', 'git-hooks', 'commit-msg-sdd-trailer.cjs');
78
+ if (fs.existsSync(remote)) return remote;
79
+ }
80
+ } catch {
81
+ // ignore
82
+ }
83
+ if (sddConfig.isMonoLayout(projectRoot)) {
84
+ const monoSpec = sddConfig.resolveMonoSpecRoot(projectRoot);
85
+ if (monoSpec) {
86
+ const nested = path.join(monoSpec, 'skywalk-sdd', 'git-hooks', 'commit-msg-sdd-trailer.cjs');
87
+ if (fs.existsSync(nested)) return nested;
88
+ }
89
+ }
90
+ return null;
91
+ }
92
+
70
93
  function checkHookInstalled(projectRoot) {
71
94
  const hookPath = path.join(projectRoot, '.git', 'hooks', 'commit-msg');
72
- const scriptPath = path.join(projectRoot, 'skywalk-sdd', 'git-hooks', 'commit-msg-sdd-trailer.cjs');
95
+ const scriptPath = resolveCommitMsgScriptPath(projectRoot);
73
96
  if (!fs.existsSync(path.join(projectRoot, '.git'))) {
74
97
  return { status: STATUS.SKIP, message: '非 Git 仓库,跳过 Hook 检查' };
75
98
  }
76
- if (!fs.existsSync(scriptPath)) {
99
+ if (!scriptPath) {
77
100
  return {
78
101
  status: STATUS.FIXABLE,
79
102
  code: 'HOOK_SCRIPT_MISSING',
80
- message: 'commit-msg 脚本未部署,可重新执行 kld-sdd-init 或手动复制模板',
103
+ message: 'commit-msg 脚本未找到(本仓或 sdd.specPath 下均无 skywalk-sdd/git-hooks/),可重新执行 kld-sdd-init',
81
104
  fix: { type: 'deploy-hook-script' },
82
105
  };
83
106
  }
@@ -169,8 +169,18 @@ function upsertTrailers(message, trailers) {
169
169
  return body;
170
170
  }
171
171
 
172
+ function resolveMsgFile(argv = process.argv) {
173
+ // Git 传入的 message 文件路径;跳过 --project= 等可选参数
174
+ for (let i = 2; i < argv.length; i += 1) {
175
+ const arg = argv[i];
176
+ if (!arg || arg.startsWith('--')) continue;
177
+ return arg;
178
+ }
179
+ return null;
180
+ }
181
+
172
182
  function main(env = process.env) {
173
- const msgFile = process.argv[2];
183
+ const msgFile = resolveMsgFile();
174
184
  if (!msgFile) {
175
185
  fail('缺少 commit message 文件参数');
176
186
  }
@@ -18,9 +18,26 @@ function parseInput(raw) {
18
18
  }
19
19
  }
20
20
 
21
+ function resolveSkywalkDir(dir) {
22
+ const direct = path.join(dir, 'skywalk-sdd');
23
+ if (fs.existsSync(path.join(direct, 'log.cjs')) || fs.existsSync(path.join(direct, 'log.js'))) {
24
+ return direct;
25
+ }
26
+ const hintFile = path.join(dir, '.sdd-spec-root');
27
+ if (fs.existsSync(hintFile)) {
28
+ const rel = String(fs.readFileSync(hintFile, 'utf8') || '').trim().split(/\r?\n/)[0].trim();
29
+ if (rel) {
30
+ const nested = path.join(dir, rel, 'skywalk-sdd');
31
+ if (fs.existsSync(path.join(nested, 'log.cjs')) || fs.existsSync(path.join(nested, 'log.js'))) {
32
+ return nested;
33
+ }
34
+ }
35
+ }
36
+ return null;
37
+ }
38
+
21
39
  function hasTelemetryCli(dir) {
22
- return fs.existsSync(path.join(dir, 'skywalk-sdd', 'log.cjs')) ||
23
- fs.existsSync(path.join(dir, 'skywalk-sdd', 'log.js'));
40
+ return Boolean(resolveSkywalkDir(dir));
24
41
  }
25
42
 
26
43
  function findTelemetryRoot(startDir) {
@@ -51,7 +68,8 @@ function findProjectRoot(input) {
51
68
  }
52
69
 
53
70
  function readActiveStages(projectRoot) {
54
- const stateDir = path.join(projectRoot, 'skywalk-sdd', 'state');
71
+ const skywalk = resolveSkywalkDir(projectRoot);
72
+ const stateDir = skywalk ? path.join(skywalk, 'state') : path.join(projectRoot, 'skywalk-sdd', 'state');
55
73
  if (!fs.existsSync(stateDir)) return [];
56
74
  return fs.readdirSync(stateDir)
57
75
  .filter(file => file.endsWith('.json'))
@@ -76,8 +94,8 @@ const activeStages = readActiveStages(projectRoot)
76
94
 
77
95
  const lines = [
78
96
  'SDD Telemetry reminder:',
79
- '- Run skywalk-sdd/log.cjs start before the OPSX stage work begins.',
80
- '- Run skywalk-sdd/log.cjs end before stopping the stage.',
97
+ '- Run node "$(cat .sdd-spec-root)/skywalk-sdd/log.cjs" start before the OPSX stage work begins.',
98
+ '- Run node "$(cat .sdd-spec-root)/skywalk-sdd/log.cjs" end before stopping the stage.',
81
99
  '- Hooks are only an enhancement; OPSX skill instructions remain authoritative.',
82
100
  ];
83
101
 
@@ -13,8 +13,27 @@ if (!/\/opsx[:|-]/.test(prompt)) {
13
13
  process.exit(0);
14
14
  }
15
15
 
16
+ function resolveSkywalkDir(dir) {
17
+ const direct = path.join(dir, 'skywalk-sdd');
18
+ if (fs.existsSync(path.join(direct, 'log.cjs')) || fs.existsSync(path.join(direct, 'log.js'))) {
19
+ return direct;
20
+ }
21
+ const hintFile = path.join(dir, '.sdd-spec-root');
22
+ if (fs.existsSync(hintFile)) {
23
+ const rel = String(fs.readFileSync(hintFile, 'utf8') || '').trim().split(/\r?\n/)[0].trim();
24
+ if (rel) {
25
+ const nested = path.join(dir, rel, 'skywalk-sdd');
26
+ if (fs.existsSync(path.join(nested, 'log.cjs')) || fs.existsSync(path.join(nested, 'log.js'))) {
27
+ return nested;
28
+ }
29
+ }
30
+ }
31
+ return null;
32
+ }
33
+
16
34
  function readActiveStages(projectRoot) {
17
- const stateDir = path.join(projectRoot, 'skywalk-sdd', 'state');
35
+ const skywalk = resolveSkywalkDir(projectRoot);
36
+ const stateDir = skywalk ? path.join(skywalk, 'state') : path.join(projectRoot, 'skywalk-sdd', 'state');
18
37
  if (!fs.existsSync(stateDir)) return [];
19
38
  return fs.readdirSync(stateDir)
20
39
  .filter((file) => file.endsWith('.json'))
@@ -35,8 +54,8 @@ const activeStages = readActiveStages(projectRoot)
35
54
 
36
55
  const lines = [
37
56
  'SDD Telemetry reminder:',
38
- '- Run skywalk-sdd/log.cjs start before the OPSX stage work begins.',
39
- '- Run skywalk-sdd/log.cjs end before stopping the stage.',
57
+ '- Run node "$(cat .sdd-spec-root)/skywalk-sdd/log.cjs" start before the OPSX stage work begins.',
58
+ '- Run node "$(cat .sdd-spec-root)/skywalk-sdd/log.cjs" end before stopping the stage.',
40
59
  '- Hooks are only an enhancement; OPSX skill instructions remain authoritative.',
41
60
  ];
42
61
 
@@ -27,15 +27,15 @@ allowed-tools:
27
27
  > - ⛔ **隔离红线**:绝对禁止加载同级其他 Capability 的文档
28
28
 
29
29
  > **🖥️ 跨平台执行规则**
30
- > - **写代码**:cwd 保持在 Git 根(含 `src/`);**文档 / telemetry**:`--project` 指向 SDD 包裹包(`node skywalk-sdd/ontology/cli.cjs spec-root` 可查看)。
31
- > - `openspec` 命令:优先 `node skywalk-sdd/openspec-shim.cjs list`(自动 cd 到包裹包)。
30
+ > - **写代码**:cwd 保持在 Git 根(含 `src/`);**文档 / telemetry**:`--project` 指向 SDD 包裹包(`node "$(cat .sdd-spec-root)/skywalk-sdd/ontology/cli.cjs" spec-root` 可查看)。
31
+ > - `openspec` 命令:优先 `node "$(cat .sdd-spec-root)/skywalk-sdd/openspec-shim.cjs" list`(自动 cd 到包裹包)。
32
32
  > - Telemetry 命令默认使用 `--project=.`,兼容 Windows、macOS、Linux。
33
33
  > - ${SHELL_GUIDANCE}
34
34
  > - 不要省略 `--source=opsx-command` 与 `--session-id=<会话ID>`。
35
35
  > **📊 Telemetry(必做,不得跳过)**
36
36
  > - 阶段开始 / 阶段结束 / `task_update` / `ai_adoption_review` / `worktree_finish` 的**完整命令模板**见 `./reference.md`「📊 Telemetry 命令模板」。
37
37
  > - 不得跳过任何 telemetry 记录;`task_update` 的 `--task-id=<TASK-ID>` 必须替换为实际任务 ID,否则 E4 指标无法计算;TDD 测试骨架任务须在 `--details-json` 带 `"task_kind":"test-skeleton"`(P3,详见 `./reference.md`)。
38
- > - 较大的 `--details-json` 负载可先写入文件,再通过 `--details-file=skywalk-sdd/state/<变更名称>-<type>.json` 传递(例如 `task_update` 对应 `<变更名称>-task-update.json`)。
38
+ > - 较大的 `--details-json` 负载可先写入文件,再通过 `--details-file="$(cat .sdd-spec-root)/skywalk-sdd/state/<变更名称>-<type>.json"` 传递(例如 `task_update` 对应 `<变更名称>-task-update.json`)。
39
39
  > - `task_update` 记录成功后会自动调用 `check-task` 更新 `tasks.md` 中的 checkbox;录入成功后,agent 可以通过 `check-task` 命令验证该 checkbox 已更新。
40
40
  > - **⚠️ P1-2 ai_adoption_review 必填 ai_diff**:记录 AI 产出快照时,`--details-json` 必须含 `ai_diff.files_changed`(即使=0)与 `ai_diff.files`(产出文件路径数组,非空),**不得只发 `assertions`**。`vcs_mode=readonly` 时 `ai_diff.added_lines` 不得为 null——用只读 `git diff --numstat HEAD` 取值(apply Git 只读策略允许);`vcs_mode=no-git` 时可填 null。工具侧已加记录时硬校验:`files` 空数组或 `readonly` 下 `added_lines=null` 将 **拒绝记录**(与 conformance_review 校验对称)。缺失 `ai_diff` 会导致 report 变更文件数为 null(工具侧已加 `assertions[].files` 兜底,但 `ai_diff` 是主数据源)。完整模板见 `./reference.md`「§5.1」。
41
41
  > - **⚠️ P1-3 process_note 必记**:阶段内发生 修复/澄清/决策/API 故障/换模型 时,**必须**记 `process_note` 事件(`kind` 见 `./reference.md`「必记节点清单」)。全程 0 条 process_note 视为过程记录缺失,report 过程记录区显示空。
@@ -14,7 +14,7 @@ description: opsx-apply 的阶段强制检查点与自检清单。仅在执行 a
14
14
  ### §1.2 Check 门禁(apply 前必做)
15
15
 
16
16
  - [ ] 读取 telemetry 事件数据,检查当前 change 是否已有完成的 check 阶段
17
- - `node skywalk-sdd/log.cjs doctor --project=. --change=<change-name>` 或搜 `skywalk-sdd/data/events/<change-name>/` 的 `stage_end` + `command=check` + `result=success`
17
+ - `node "$(cat .sdd-spec-root)/skywalk-sdd/log.cjs" doctor --project=. --change=<change-name>` 或搜 `skywalk-sdd/data/events/<change-name>/` 的 `stage_end` + `command=check` + `result=success`
18
18
  - [ ] check 已完成 ✅ → 继续 apply 流程
19
19
  - [ ] check 未完成 ❌ → **强制拒绝** apply,引导用户先执行 `/opsx-check <change-name>`(不得跳过)
20
20
  - [ ] telemetry 数据缺失(新项目无历史) → 提示首次执行先运行 `/opsx-check`,询问是否现在执行
@@ -82,7 +82,7 @@ description: opsx-apply 的阶段强制检查点与自检清单。仅在执行 a
82
82
  - [ ] 两种格式同步更新,不可遗漏
83
83
  - [ ] 显示进度:`✅ [TASK-ID] 已完成 [N/M]`
84
84
  - [ ] 记录任务级 Telemetry(`task_update`,`--task-id=<TASK-ID>` 必填,否则 E4 指标无法计算);TDD 测试骨架任务须在 `--details-json` 带 `"task_kind":"test-skeleton"`(P3,避免红灯误判拉低 E4)
85
- - [ ] ⛔ **task_update 后必须验证 checkbox 已更新**:执行 `node skywalk-sdd/index.cjs check-task --project=. --change=<变更名称> --task-id=<TASK-ID>` 确认 tasks.md 中对应行已从 `- [ ]` 变为 `- [x]`;若未更新,手动修改 tasks.md 并报告
85
+ - [ ] ⛔ **task_update 后必须验证 checkbox 已更新**:执行 `node "$(cat .sdd-spec-root)/skywalk-sdd/index.cjs" check-task --project=. --change=<变更名称> --task-id=<TASK-ID>` 确认 tasks.md 中对应行已从 `- [ ]` 变为 `- [x]`;若未更新,手动修改 tasks.md 并报告
86
86
 
87
87
  ### §5f.1 apply 结束前 checkbox 全量同步校验
88
88
 
@@ -112,7 +112,7 @@ description: opsx-apply 的阶段强制检查点与自检清单。仅在执行 a
112
112
  - [ ] 主仓库无与 merge 冲突的未跟踪文件(见 §1.5 Step 0.5)
113
113
  - [ ] `test-strategy` 为 `tdd`/`impl-first` 时,§6.0 测试证据已存在
114
114
  - [ ] 在**主仓库根目录**执行收尾脚本(禁止在 `.worktrees/...` 内执行)
115
- - [ ] 执行:`node skywalk-sdd/apply-worktree-finish.cjs --change=<变更名称> --capability=<capability-name>`
115
+ - [ ] 执行:`node "$(cat .sdd-spec-root)/skywalk-sdd/apply-worktree-finish.cjs" --change=<变更名称> --capability=<capability-name>`
116
116
  - [ ] 默认 merge 目标 = §1.5 `--record-base` 写入的 `integration_base`(**不是**默认 master)
117
117
  - [ ] 若 §1.5 判定未建 worktree(串行、强依赖、沙箱回退)→ 跳过本节
118
118