kld-sdd 2.6.9 → 2.6.11

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/lib/init.js CHANGED
@@ -687,14 +687,13 @@ function cleanupNativeOpenspecSkills(selectedTools = Object.keys(TOOL_CONFIGS))
687
687
  }
688
688
 
689
689
  /**
690
- * 复制标准文档模版到项目
690
+ * 复制标准文档模版到项目(openspec-templates/)
691
691
  */
692
- function copyTemplatesToProject() {
692
+ function copyTemplatesToProject(cwd = process.cwd()) {
693
693
  console.log('📄 正在复制标准文档模版到项目...');
694
694
 
695
695
  const pkgPath = getPackagePath();
696
696
  const templatePath = path.join(pkgPath, 'templates', 'openspec');
697
- const cwd = process.cwd();
698
697
  const targetDir = path.join(cwd, 'openspec-templates');
699
698
 
700
699
  if (!fs.existsSync(templatePath)) {
@@ -702,12 +701,10 @@ function copyTemplatesToProject() {
702
701
  return false;
703
702
  }
704
703
 
705
- // 创建目标目录
706
704
  if (!fs.existsSync(targetDir)) {
707
705
  fs.mkdirSync(targetDir, { recursive: true });
708
706
  }
709
707
 
710
- // 复制模版文件
711
708
  copyDir(templatePath, targetDir);
712
709
 
713
710
  console.log(`✅ 标准文档模版已复制到: ${targetDir}`);
@@ -724,29 +721,24 @@ function copyTemplatesToProject() {
724
721
  * 初始化全局 overview.md 到 openspec/specs/ 目录
725
722
  * overview.md 是全局架构约束,所有 Capability 的设计都必须遵守
726
723
  */
727
- function initGlobalOverview() {
724
+ function initGlobalOverview(cwd = process.cwd()) {
728
725
  console.log('🌐 正在初始化全局架构约束 (overview.md)...');
729
726
 
730
727
  const pkgPath = getPackagePath();
731
728
  const overviewSource = path.join(pkgPath, 'templates', 'openspec', 'overview.md');
732
- const cwd = process.cwd();
733
729
  const targetDir = path.join(cwd, 'openspec', 'specs');
734
730
  const targetFile = path.join(targetDir, 'overview.md');
735
731
 
736
- // 检查源文件是否存在
737
732
  if (!fs.existsSync(overviewSource)) {
738
733
  console.log(`⚠️ overview.md 源文件不存在: ${overviewSource}`);
739
734
  return false;
740
735
  }
741
736
 
742
- // 创建目标目录
743
737
  if (!fs.existsSync(targetDir)) {
744
738
  fs.mkdirSync(targetDir, { recursive: true });
745
739
  }
746
740
 
747
- // 检查目标文件是否已存在
748
741
  if (fs.existsSync(targetFile)) {
749
- // 读取现有文件,检查是否需要更新
750
742
  const existingContent = fs.readFileSync(targetFile, 'utf-8');
751
743
  const newContent = fs.readFileSync(overviewSource, 'utf-8');
752
744
 
@@ -755,13 +747,11 @@ function initGlobalOverview() {
755
747
  return true;
756
748
  }
757
749
 
758
- // 备份现有文件
759
750
  const backupFile = path.join(targetDir, 'overview.md.backup');
760
751
  fs.copyFileSync(targetFile, backupFile);
761
752
  console.log(` ℹ️ 已备份现有 overview.md 到 ${backupFile}`);
762
753
  }
763
754
 
764
- // 复制 overview.md
765
755
  fs.copyFileSync(overviewSource, targetFile);
766
756
 
767
757
  console.log(`✅ 全局架构约束已初始化: ${targetFile}`);
@@ -857,14 +847,22 @@ function deployTelemetryDataDir(targetCwd = process.cwd()) {
857
847
  console.log(' ✓ 部署 skywalk-sdd/apply-worktree-finish.cjs(Apply 收尾脚本)');
858
848
  }
859
849
 
850
+ const openspecShimSrc = path.join(pkgPath, 'skywalk-sdd', 'openspec-shim.cjs');
851
+ const openspecShimDst = path.join(dataDir, 'openspec-shim.cjs');
852
+ if (fs.existsSync(openspecShimSrc)) {
853
+ fs.copyFileSync(openspecShimSrc, openspecShimDst);
854
+ console.log(' ✓ 部署 skywalk-sdd/openspec-shim.cjs(openspec 包裹包路径 shim)');
855
+ }
856
+
860
857
  console.log(' ✓ 调用方式: node skywalk-sdd/log.cjs start|end|metrics|semantic-identity|semantic-reconcile|semantic-check');
858
+ console.log(' ✓ openspec: node skywalk-sdd/openspec-shim.cjs list(自动 cd 到 *-sdd-specs)');
861
859
  console.log(' ✓ Spec Context: node skywalk-sdd/context-client.cjs --query="<自然语言需求>" --target-stage=spec');
862
860
  console.log(' ✓ Apply worktree: record-base 在 §1.5;收尾 apply-worktree-finish.cjs --change=<name>');
863
861
  console.log('✅ Telemetry 已就绪(数据存储在 skywalk-sdd/,无需配置 MCP)');
864
862
  return true;
865
863
  }
866
864
 
867
- function installGitHookShim(cwd, hookName, scriptPath) {
865
+ function installGitHookShim(cwd, hookName, scriptPath, options = {}) {
868
866
  const hooksDir = path.join(cwd, '.git', 'hooks');
869
867
  if (!fs.existsSync(hooksDir)) {
870
868
  return;
@@ -872,11 +870,20 @@ function installGitHookShim(cwd, hookName, scriptPath) {
872
870
 
873
871
  const hookPath = path.join(hooksDir, hookName);
874
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}" "$@"`;
875
879
  const shim = [
876
880
  '#!/bin/sh',
877
881
  `# ${marker}`,
878
882
  `if [ -f "${scriptPath}" ]; then`,
879
- ` node "${scriptPath}" "$@"`,
883
+ nodeLine,
884
+ 'else',
885
+ ` echo "[kld-sdd] Hook 脚本不存在: ${scriptPath}" >&2`,
886
+ ' exit 1',
880
887
  'fi',
881
888
  '',
882
889
  ].join('\n');
@@ -884,10 +891,21 @@ function installGitHookShim(cwd, hookName, scriptPath) {
884
891
  if (fs.existsSync(hookPath)) {
885
892
  const existing = fs.readFileSync(hookPath, 'utf8');
886
893
  if (existing.includes(marker)) {
887
- console.log(` ✓ .git/hooks/${hookName} 已包含 SDD 质量门禁`);
888
- } else {
889
- 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;
890
907
  }
908
+ console.log(` ℹ️ .git/hooks/${hookName} 已存在,已保留不覆盖;可手动调用 ${scriptPath}`);
891
909
  return;
892
910
  }
893
911
 
@@ -900,14 +918,52 @@ function installGitHookShim(cwd, hookName, scriptPath) {
900
918
  console.log(` ✓ 安装 .git/hooks/${hookName} SDD 质量门禁`);
901
919
  }
902
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
+
903
959
  /**
904
960
  * 部署 Git hooks / CI 兜底模板
905
961
  * 这些脚本只做质量门禁和 CI 补充记录,不替代 OPSX 主采集链路。
906
962
  *
907
963
  * @param {string} [targetCwd]
908
964
  * @param {{ mode?: 'full'|'commit-msg-only' }} [options]
909
- * full: spec 仓(pre-commit/pre-push/commit-msg + CI)
910
- * 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)
911
967
  */
912
968
  function deployQualityGateTemplates(targetCwd = process.cwd(), options = {}) {
913
969
  const pkgPath = getPackagePath();
@@ -948,12 +1004,12 @@ function deployQualityGateTemplates(targetCwd = process.cwd(), options = {}) {
948
1004
  }
949
1005
 
950
1006
  /**
951
- * 代码仓轻量接入:只部署 Hook 运行所需 ontology + commit-msg,不部署 skills
1007
+ * 代码仓轻量接入:只写 .sdd.yaml + sdd.specPath + .git/hooks/commit-msg。
1008
+ * 不在代码仓落 skywalk-sdd/——Hook 脚本与 ontology 引用 spec 包裹包那一份。
952
1009
  */
953
1010
  function attachCodeRepoLite(codeRepoRoot, specRepoRoot, options = {}) {
954
1011
  const cwd = path.resolve(codeRepoRoot);
955
1012
  const specRoot = path.resolve(specRepoRoot);
956
- const pkgPath = options.pkgPath || getPackagePath();
957
1013
  const label = path.basename(cwd);
958
1014
 
959
1015
  if (!workspaceLayout.isGitRepo(cwd)) {
@@ -963,16 +1019,7 @@ function attachCodeRepoLite(codeRepoRoot, specRepoRoot, options = {}) {
963
1019
  return { ok: false, repo: label, message: `spec 路径不是 Git 仓库: ${specRoot}` };
964
1020
  }
965
1021
 
966
- console.log(`🔗 轻量接入代码仓(无 skills): ${label}`);
967
-
968
- // Hook 依赖 ontology + git-hooks;不部署完整 telemetry/skills
969
- const dataDir = path.join(cwd, 'skywalk-sdd');
970
- const ontologySrc = path.join(pkgPath, 'skywalk-sdd', 'ontology');
971
- const ontologyDst = path.join(dataDir, 'ontology');
972
- if (fs.existsSync(ontologySrc)) {
973
- copyDir(ontologySrc, ontologyDst);
974
- }
975
- deployQualityGateTemplates(cwd, { mode: 'commit-msg-only' });
1022
+ console.log(`🔗 轻量接入代码仓(无 skywalk-sdd/、无 skills): ${label}`);
976
1023
 
977
1024
  const remote = sddConfig.gitRemoteUrl(specRoot) || 'git@gitlab.example.com:biz/xxx-sdd-specs.git';
978
1025
  const sddYamlPath = path.join(cwd, '.sdd.yaml');
@@ -998,6 +1045,9 @@ function attachCodeRepoLite(codeRepoRoot, specRepoRoot, options = {}) {
998
1045
  }
999
1046
  console.log(` ✓ sdd.specPath = ${linked.path}`);
1000
1047
 
1048
+ installCodeRepoCommitMsgHook(cwd, specRoot, options);
1049
+ console.log(' ✓ commit-msg → spec 仓 skywalk-sdd/git-hooks/(代码仓不落 skywalk-sdd/)');
1050
+
1001
1051
  return { ok: true, repo: label, path: cwd, specPath: linked.path };
1002
1052
  }
1003
1053
 
@@ -1065,32 +1115,190 @@ function deployDotSddYaml(cwd = process.cwd(), pkgPath = getPackagePath()) {
1065
1115
  }
1066
1116
 
1067
1117
  /**
1068
- * 单仓(openspec 与代码同仓)声明 layout: mono
1118
+ * 单仓声明 layout: mono,并记录仓内统一包裹目录相对路径。
1069
1119
  * commit-msg Hook 靠它跳过「spec 工作区干净」检查并只写 Spec-Change。
1070
1120
  */
1071
- function deployMonoSddYaml(cwd = process.cwd()) {
1121
+ function deployMonoSddYaml(cwd = process.cwd(), options = {}) {
1072
1122
  const target = path.join(cwd, '.sdd.yaml');
1123
+ const specRel = options.specPathRelative
1124
+ ? String(options.specPathRelative).replace(/\\/g, '/')
1125
+ : '';
1126
+
1073
1127
  if (fs.existsSync(target)) {
1074
1128
  const existing = fs.readFileSync(target, 'utf8');
1075
1129
  if (/^layout:\s*mono\s*$/m.test(existing)) {
1130
+ if (specRel && !new RegExp(`^spec_path:\\s*${specRel.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*$`, 'm').test(existing)) {
1131
+ // 已有 mono 但缺/错 spec_path:补写相对路径
1132
+ let next = existing.replace(/\s*$/, '');
1133
+ if (/^spec_path:\s*.+$/m.test(next)) {
1134
+ next = next.replace(/^spec_path:\s*.+$/m, `spec_path: ${specRel}`);
1135
+ } else {
1136
+ next = `${next}\nspec_path: ${specRel}`;
1137
+ }
1138
+ fs.writeFileSync(target, `${next}\n`, 'utf8');
1139
+ console.log(` ✓ 已更新 .sdd.yaml spec_path → ${specRel}`);
1140
+ return true;
1141
+ }
1076
1142
  console.log(' ✓ .sdd.yaml 已声明 layout: mono,跳过');
1077
1143
  return true;
1078
1144
  }
1079
- console.log('⚠️ .sdd.yaml 已存在但未声明 layout: mono;单仓请手动补一行 `layout: mono`');
1145
+ console.log('⚠️ .sdd.yaml 已存在但未声明 layout: mono;单仓请手动补 `layout: mono`');
1080
1146
  return false;
1081
1147
  }
1082
1148
  const lines = [
1083
- '# 单仓布局:openspec 文档与代码在同一个仓库',
1084
- '# commit-msg 只写 Spec-Change(spec 与代码同属一次 commit,无需 Spec-Revision 指针)',
1149
+ '# 单仓布局:业务代码与 SDD 包裹包在同一个 Git 仓库',
1150
+ '# SDD 内容统一在 spec_path 目录内(openspec / modules / sdd.config / 模版)',
1151
+ '# commit-msg 只写 Spec-Change(同仓一次 commit,无需 Spec-Revision 指针)',
1085
1152
  'version: 1',
1086
1153
  'layout: mono',
1087
- '',
1088
1154
  ];
1155
+ if (specRel) lines.push(`spec_path: ${specRel}`);
1156
+ lines.push('');
1089
1157
  fs.writeFileSync(target, lines.join('\n'), 'utf8');
1090
- console.log(`✅ 已创建 .sdd.yaml(layout: mono): ${target}`);
1158
+ console.log(`✅ 已创建 .sdd.yaml(layout: mono${specRel ? `, spec_path: ${specRel}` : ''}): ${target}`);
1091
1159
  return true;
1092
1160
  }
1093
1161
 
1162
+ /**
1163
+ * 向统一 SDD 包裹目录写入完整内容(多仓兄弟仓 / 单仓内部子目录共用)。
1164
+ * 不在此部署编辑器 skills——skills 只装工作目录或单仓 Git 根。
1165
+ */
1166
+ async function populateSpecPackage(specRoot, options = {}) {
1167
+ const root = path.resolve(specRoot);
1168
+ const skipOpenspec = Boolean(options.skipOpenspec);
1169
+ const asIndependentGit = Boolean(options.asIndependentGit);
1170
+ console.log(`📦 正在填充 SDD 包裹包: ${root}`);
1171
+
1172
+ if (!skipOpenspec) {
1173
+ const prev = process.cwd();
1174
+ try {
1175
+ process.chdir(root);
1176
+ // 包裹包内只建 openspec 目录结构,不把编辑器配置写进文档仓
1177
+ await runOpenspecInit([]);
1178
+ } finally {
1179
+ process.chdir(prev);
1180
+ }
1181
+ }
1182
+
1183
+ deployTelemetryDataDir(root);
1184
+ if (asIndependentGit || workspaceLayout.isGitRepo(root)) {
1185
+ deployQualityGateTemplates(root, { mode: 'full' });
1186
+ }
1187
+ deployModulesYaml(root);
1188
+ deploySddConfigYaml(root);
1189
+ copyTemplatesToProject(root);
1190
+ initGlobalOverview(root);
1191
+ deploySddGuideManual(root);
1192
+ console.log(`✅ SDD 包裹包已就绪: ${path.basename(root)}`);
1193
+ return { ok: true, path: root };
1194
+ }
1195
+
1196
+ /**
1197
+ * 在工作区 / Git 根写入 .sdd-spec-root,供 shell 与 skills 引用相对包裹包路径。
1198
+ */
1199
+ function writeSddSpecRootHint(anchorDir, specPackageAbs) {
1200
+ const anchor = path.resolve(anchorDir);
1201
+ const specAbs = path.resolve(specPackageAbs);
1202
+ const rel = path.relative(anchor, specAbs).replace(/\\/g, '/');
1203
+ if (!rel || rel.startsWith('..')) return null;
1204
+ const filePath = path.join(anchor, '.sdd-spec-root');
1205
+ fs.writeFileSync(filePath, `${rel}\n`, 'utf8');
1206
+ console.log(` ✓ 已写入 .sdd-spec-root → ${rel}`);
1207
+ return filePath;
1208
+ }
1209
+
1210
+ /**
1211
+ * 工作区 / 单仓 Git 根的 skywalk-sdd/ 改为指向包裹包内唯一实体(符号链接)。
1212
+ * skills 仍可用 `node skywalk-sdd/log.cjs`,实体文件只在 *-sdd-specs/skywalk-sdd/。
1213
+ * 链接失败时回退为薄转发脚本(不复制 ontology)。
1214
+ */
1215
+ function deploySkywalkForwarders(anchorDir, specPackageAbs) {
1216
+ const anchor = path.resolve(anchorDir);
1217
+ const specAbs = path.resolve(specPackageAbs);
1218
+ const relSpec = path.relative(anchor, specAbs).replace(/\\/g, '/');
1219
+ const skywalkDir = path.join(anchor, 'skywalk-sdd');
1220
+ const targetSkywalk = path.join(specAbs, 'skywalk-sdd');
1221
+ fs.mkdirSync(path.join(skywalkDir, 'ontology'), { recursive: true });
1222
+
1223
+ const writeForwarder = (destRel, targetAbsPath) => {
1224
+ const dest = path.join(skywalkDir, destRel);
1225
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
1226
+ const relFromDestDir = path.relative(path.dirname(dest), targetAbsPath).replace(/\\/g, '/');
1227
+ const body = [
1228
+ '#!/usr/bin/env node',
1229
+ "'use strict';",
1230
+ "const { spawnSync } = require('child_process');",
1231
+ "const path = require('path');",
1232
+ `const target = path.resolve(__dirname, ${JSON.stringify(relFromDestDir)});`,
1233
+ 'const r = spawnSync(process.execPath, [target, ...process.argv.slice(2)], { stdio: \'inherit\' });',
1234
+ 'process.exit(r.status === null ? 1 : r.status);',
1235
+ '',
1236
+ ].join('\n');
1237
+ fs.writeFileSync(dest, body, 'utf8');
1238
+ };
1239
+
1240
+ const entries = [
1241
+ 'log.cjs',
1242
+ 'openspec-shim.cjs',
1243
+ 'context-client.cjs',
1244
+ 'apply-worktree-finish.cjs',
1245
+ ];
1246
+ for (const name of entries) {
1247
+ writeForwarder(name, path.join(targetSkywalk, name));
1248
+ }
1249
+ writeForwarder(path.join('ontology', 'cli.cjs'), path.join(targetSkywalk, 'ontology', 'cli.cjs'));
1250
+ console.log(` ✓ skywalk-sdd/ 转发脚本 → ${relSpec}/skywalk-sdd/(实体在包裹包)`);
1251
+ return { ok: true, mode: 'forwarders', relSpec };
1252
+ }
1253
+
1254
+ function linkSkywalkToSpecPackage(anchorDir, specPackageAbs) {
1255
+ const anchor = path.resolve(anchorDir);
1256
+ const targetAbs = path.join(path.resolve(specPackageAbs), 'skywalk-sdd');
1257
+ const linkAbs = path.join(anchor, 'skywalk-sdd');
1258
+
1259
+ if (!fs.existsSync(targetAbs)) {
1260
+ console.log(' ⚠️ 包裹包尚无 skywalk-sdd/,跳过工作区链接');
1261
+ return { ok: false, message: 'spec skywalk-sdd missing' };
1262
+ }
1263
+
1264
+ try {
1265
+ let st = null;
1266
+ try {
1267
+ st = fs.lstatSync(linkAbs);
1268
+ } catch (err) {
1269
+ if (!err || err.code !== 'ENOENT') throw err;
1270
+ }
1271
+ if (st) {
1272
+ if (st.isSymbolicLink()) {
1273
+ const current = fs.realpathSync(linkAbs);
1274
+ if (current === fs.realpathSync(targetAbs)) {
1275
+ console.log(' ✓ skywalk-sdd/ 已链接到包裹包(唯一实体)');
1276
+ return { ok: true, mode: 'symlink', linkAbs, targetAbs };
1277
+ }
1278
+ fs.unlinkSync(linkAbs);
1279
+ } else if (st.isDirectory()) {
1280
+ // 旧完整副本 → 删除后改链接(权威数据在包裹包)
1281
+ fs.rmSync(linkAbs, { recursive: true, force: true });
1282
+ } else {
1283
+ fs.unlinkSync(linkAbs);
1284
+ }
1285
+ }
1286
+ } catch (err) {
1287
+ console.log(` ⚠️ 清理旧 skywalk-sdd/ 失败: ${err.message}`);
1288
+ return deploySkywalkForwarders(anchor, specPackageAbs);
1289
+ }
1290
+
1291
+ const rel = path.relative(anchor, targetAbs);
1292
+ try {
1293
+ fs.symlinkSync(rel, linkAbs, process.platform === 'win32' ? 'junction' : 'dir');
1294
+ console.log(` ✓ skywalk-sdd/ → ${rel.replace(/\\/g, '/')}(唯一实体在包裹包)`);
1295
+ return { ok: true, mode: 'symlink', linkAbs, targetAbs, rel };
1296
+ } catch (err) {
1297
+ console.log(` ⚠️ 符号链接失败 (${err.message}),回退为转发脚本`);
1298
+ return deploySkywalkForwarders(anchor, specPackageAbs);
1299
+ }
1300
+ }
1301
+
1094
1302
  /**
1095
1303
  * 检测当前项目已初始化的 AI 编辑器
1096
1304
  */
@@ -1121,6 +1329,8 @@ function updateGitignore() {
1121
1329
  '.agents/commands/personal-*',
1122
1330
  'skywalk-sdd/events/',
1123
1331
  'skywalk-sdd/state/',
1332
+ '**/skywalk-sdd/events/',
1333
+ '**/skywalk-sdd/state/',
1124
1334
  '.worktrees/'
1125
1335
  ];
1126
1336
 
@@ -1134,9 +1344,11 @@ function updateGitignore() {
1134
1344
  .workbuddy/commands/personal-*
1135
1345
  .agents/commands/personal-*
1136
1346
 
1137
- # SDD Telemetry 数据(本地度量,可选提交;本体 JSON 与报告随 openspec/changes/<change>/ 提交)
1347
+ # SDD Telemetry 数据(实体在 *-sdd-specs/skywalk-sdd/;工作区可能是 symlink)
1138
1348
  skywalk-sdd/events/
1139
1349
  skywalk-sdd/state/
1350
+ **/skywalk-sdd/events/
1351
+ **/skywalk-sdd/state/
1140
1352
 
1141
1353
  # SDD Apply 隔离 worktree(本地临时目录)
1142
1354
  .worktrees/
@@ -1274,32 +1486,21 @@ async function main() {
1274
1486
  }
1275
1487
 
1276
1488
  const workspaceRoot = process.cwd();
1277
- const layout = workspaceLayout.detectWorkspaceLayout(workspaceRoot);
1489
+ let layout = workspaceLayout.detectWorkspaceLayout(workspaceRoot);
1490
+ let specPackage = layout.specRepo
1491
+ ? { name: layout.specRepo.name, abs: layout.specRepo.abs, created: false, isGit: true }
1492
+ : null;
1493
+
1278
1494
  if (layout.isWorkspace) {
1279
1495
  console.log('📂 检测到个人工作目录布局(子目录 Git 仓库)');
1280
- console.log(` Spec: ${layout.specRepo ? layout.specRepo.name : '(未发现)'}`);
1496
+ console.log(` Spec: ${specPackage ? specPackage.name : '(未发现,将自动创建包裹包)'}`);
1281
1497
  console.log(` 代码仓: ${layout.codeRepos.map((c) => c.name).join(', ') || '(无)'}`);
1282
- console.log(' → skills 只部署到本工作目录;代码仓仅轻量接入 Hook/关联');
1498
+ console.log(' → skills 只部署到本工作目录;SDD 文档统一进 *-sdd-specs 包裹包');
1283
1499
  console.log();
1284
1500
  }
1285
1501
 
1286
- // 1. openspec init:工作区模式下若有 spec 仓则在其内执行
1287
- if (!skipOpenspec) {
1288
- if (layout.isWorkspace && layout.specRepo) {
1289
- const prev = process.cwd();
1290
- try {
1291
- process.chdir(layout.specRepo.abs);
1292
- await runOpenspecInit(selectedTools);
1293
- } finally {
1294
- process.chdir(prev);
1295
- }
1296
- } else {
1297
- await runOpenspecInit(selectedTools);
1298
- }
1299
- }
1300
-
1301
1502
  if (!skipTemplate) {
1302
- // 2/3. skills 与编辑器产物:永远只装在当前工作目录一次
1503
+ // skills / 编辑器产物:永远只装在当前工作目录一次
1303
1504
  cleanupNativeOpenspecCommands(selectedTools);
1304
1505
  cleanupLegacyBundledOpsxSkills(selectedTools);
1305
1506
  deployOpsxSkills(selectedTools);
@@ -1307,49 +1508,95 @@ async function main() {
1307
1508
  deployProfileArtifacts(selectedTools, workspaceRoot, getPackagePath());
1308
1509
 
1309
1510
  if (layout.isWorkspace) {
1310
- // 工作目录本身保留一份 telemetry,供 skills 调用
1311
- deployTelemetryDataDir(workspaceRoot);
1312
-
1313
- if (layout.specRepo) {
1314
- const prev = process.cwd();
1315
- try {
1316
- process.chdir(layout.specRepo.abs);
1317
- deployTelemetryDataDir(layout.specRepo.abs);
1318
- deployQualityGateTemplates(layout.specRepo.abs, { mode: 'full' });
1319
- deployModulesYaml(layout.specRepo.abs);
1320
- deploySddConfigYaml(layout.specRepo.abs);
1321
- copyTemplatesToProject();
1322
- initGlobalOverview();
1323
- deploySddGuideManual(layout.specRepo.abs);
1324
- } finally {
1325
- process.chdir(prev);
1326
- }
1511
+ if (!specPackage) {
1512
+ console.log('📦 未发现 spec 仓,正在创建统一 SDD 包裹包(独立 Git)...');
1513
+ specPackage = workspaceLayout.ensureSpecPackage(workspaceRoot, { initGit: true });
1514
+ console.log(` ✓ ${specPackage.created ? '已创建' : '已复用'}: ${specPackage.name}`);
1515
+ }
1327
1516
 
1328
- console.log();
1329
- console.log('🔗 正在为子代码仓做轻量接入(不安装 skills)...');
1330
- for (const repo of layout.codeRepos) {
1331
- attachCodeRepoLite(repo.abs, layout.specRepo.abs);
1332
- }
1333
- const wsFile = workspaceLayout.writeWorkspaceFile(workspaceRoot, layout);
1334
- console.log(`✅ 工作区清单: ${wsFile}`);
1335
- } else {
1336
- console.log('⚠️ 未发现 spec 仓,已跳过代码仓 Hook 接入。请先准备 *-sdd-specs 子目录后执行: kld-sdd sync-repos');
1517
+ await populateSpecPackage(specPackage.abs, {
1518
+ skipOpenspec,
1519
+ asIndependentGit: true,
1520
+ });
1521
+ writeSddSpecRootHint(workspaceRoot, specPackage.abs);
1522
+ // 工作区不复制 skywalk-sdd 实体,只链到包裹包(skills 路径仍可用)
1523
+ linkSkywalkToSpecPackage(workspaceRoot, specPackage.abs);
1524
+
1525
+ // 重新探测,刷新 layout.specRepo / codeRepos
1526
+ layout = workspaceLayout.detectWorkspaceLayout(workspaceRoot);
1527
+ if (!layout.specRepo) {
1528
+ layout.specRepo = { name: specPackage.name, abs: specPackage.abs };
1337
1529
  }
1530
+
1531
+ console.log();
1532
+ console.log('🔗 正在为子代码仓做轻量接入(不安装 skills)...');
1533
+ for (const repo of layout.codeRepos) {
1534
+ attachCodeRepoLite(repo.abs, specPackage.abs);
1535
+ }
1536
+ const wsFile = workspaceLayout.writeWorkspaceFile(workspaceRoot, layout);
1537
+ console.log(`✅ 工作区清单: ${wsFile}`);
1338
1538
  } else {
1339
- // 单仓模式(旧行为):当前目录既是项目根
1340
- deployTelemetryDataDir(workspaceRoot);
1341
- deployQualityGateTemplates(workspaceRoot, { mode: 'full' });
1342
- deployModulesYaml(workspaceRoot);
1343
- deploySddConfigYaml(workspaceRoot);
1344
- if (workspaceLayout.isGitRepo(workspaceRoot)) {
1345
- // openspec 与代码同仓:显式声明单仓,Hook 才会写 Spec-Change
1346
- deployMonoSddYaml(workspaceRoot);
1539
+ // 单仓:Git 根只放代码 + skills + Hook;SDD 内容进统一包裹子目录
1540
+ const isGit = workspaceLayout.isGitRepo(workspaceRoot);
1541
+
1542
+ if (isGit) {
1543
+ console.log('📦 单仓模式:创建/复用仓内统一 SDD 包裹目录...');
1544
+ specPackage = workspaceLayout.ensureSpecPackage(workspaceRoot, { initGit: false });
1545
+ console.log(` ✓ ${specPackage.created ? '已创建' : '已复用'}: ${specPackage.name}`);
1546
+
1547
+ await populateSpecPackage(specPackage.abs, {
1548
+ skipOpenspec,
1549
+ asIndependentGit: false,
1550
+ });
1551
+ writeSddSpecRootHint(workspaceRoot, specPackage.abs);
1552
+ linkSkywalkToSpecPackage(workspaceRoot, specPackage.abs);
1553
+
1554
+ deployMonoSddYaml(workspaceRoot, { specPathRelative: specPackage.name });
1555
+ // Hook 装在 Git 根;经 symlink 写入包裹包内的 git-hooks
1556
+ deployQualityGateTemplates(workspaceRoot, { mode: 'commit-msg-only' });
1557
+ const linked = sddConfig.setSpecPath(workspaceRoot, specPackage.abs, {
1558
+ allowNestedPackage: true,
1559
+ });
1560
+ if (linked.ok) {
1561
+ console.log(` ✓ sdd.specPath = ${linked.path}`);
1562
+ } else {
1563
+ console.log(` ⚠️ 未能写入 sdd.specPath: ${linked.message}`);
1564
+ }
1347
1565
  } else {
1566
+ // 非 Git 目录:仍创建包裹包,便于随后 git init
1567
+ console.log('📦 当前目录不是 Git 仓:仍创建 SDD 包裹包(请稍后 git init)...');
1568
+ specPackage = workspaceLayout.ensureSpecPackage(workspaceRoot, { initGit: false });
1569
+ await populateSpecPackage(specPackage.abs, {
1570
+ skipOpenspec,
1571
+ asIndependentGit: false,
1572
+ });
1573
+ writeSddSpecRootHint(workspaceRoot, specPackage.abs);
1574
+ linkSkywalkToSpecPackage(workspaceRoot, specPackage.abs);
1348
1575
  deployDotSddYaml(workspaceRoot);
1349
1576
  }
1350
- copyTemplatesToProject();
1351
- initGlobalOverview();
1352
- deploySddGuideManual(workspaceRoot);
1577
+ }
1578
+ } else if (!skipOpenspec) {
1579
+ // 仅 openspec、跳过模版时:仍尊重包裹包,避免在工作区根平铺
1580
+ if (layout.isWorkspace) {
1581
+ if (!specPackage) {
1582
+ specPackage = workspaceLayout.ensureSpecPackage(workspaceRoot, { initGit: true });
1583
+ }
1584
+ const prev = process.cwd();
1585
+ try {
1586
+ process.chdir(specPackage.abs);
1587
+ await runOpenspecInit([]);
1588
+ } finally {
1589
+ process.chdir(prev);
1590
+ }
1591
+ } else {
1592
+ specPackage = workspaceLayout.ensureSpecPackage(workspaceRoot, { initGit: false });
1593
+ const prev = process.cwd();
1594
+ try {
1595
+ process.chdir(specPackage.abs);
1596
+ await runOpenspecInit([]);
1597
+ } finally {
1598
+ process.chdir(prev);
1599
+ }
1353
1600
  }
1354
1601
  }
1355
1602
 
@@ -1369,23 +1616,27 @@ async function main() {
1369
1616
  console.log('工作区部署结果:');
1370
1617
  console.log(' 🎯 工作目录 .*/skills/opsx-*/ # skills 只装这一份');
1371
1618
  console.log(' 📋 .sdd-workspace.yaml # 子仓编排清单');
1372
- if (layout.specRepo) {
1373
- console.log(` 📄 ${layout.specRepo.name}/modules.yaml + openspec-templates`);
1619
+ console.log(' 🔗 skywalk-sdd/ → *-sdd-specs/skywalk-sdd/ # 符号链接,实体只在包裹包');
1620
+ if (specPackage) {
1621
+ console.log(` 📄 ${specPackage.name}/ # 统一 SDD 包裹包(独立 Git)`);
1622
+ console.log(' modules.yaml / sdd.config.yaml / openspec / openspec-templates / skywalk-sdd');
1374
1623
  }
1375
1624
  for (const repo of layout.codeRepos) {
1376
- console.log(` 🔗 ${repo.name}/ ← commit-msg Hook + .sdd.yaml(无 skills)`);
1625
+ console.log(` 🔗 ${repo.name}/ ← commit-msg Hook + .sdd.yaml(无 skywalk-sdd/、无 skills)`);
1377
1626
  }
1378
1627
  console.log();
1379
1628
  console.log('新代码仓加入后执行:');
1380
1629
  console.log(' kld-sdd sync-repos');
1381
1630
  console.log('Trailer 仍在各代码仓 git commit 时由 Hook 写入。');
1382
1631
  } else {
1383
- console.log('已生成/覆盖:');
1384
- console.log(' 🌐 openspec/specs/overview.md # 全局架构约束(数据字典、接口规范)');
1385
- console.log(' 📖 openspec/kld-sdd操作手册.html # SDD 操作手册(HTML)');
1386
- console.log(' 📄 openspec-templates/ # 标准文档模版(参考用)');
1387
- console.log(' 📦 modules.yaml / .sdd.yaml # 多仓库命名与关联配置');
1388
- console.log(' 🎯 .*/skills/opsx-*/ # SDD skills(扁平一层)');
1632
+ console.log('单仓部署结果:');
1633
+ console.log(' 🎯 .*/skills/opsx-*/ # SDD skills(装在 Git 根)');
1634
+ if (specPackage) {
1635
+ console.log(` 📄 ${specPackage.name}/ # 统一 SDD 包裹包(仓内子目录)`);
1636
+ console.log(' modules.yaml / sdd.config.yaml / openspec / openspec-templates / skywalk-sdd');
1637
+ }
1638
+ console.log(' 🔗 .sdd.yaml (layout: mono) + commit-msg Hook');
1639
+ console.log(' 🔗 skywalk-sdd/ → *-sdd-specs/skywalk-sdd/ # 符号链接,实体只在包裹包');
1389
1640
  }
1390
1641
  if (selectedTools.includes('kunlunzhima')) {
1391
1642
  console.log(' 📎 .kunlunzhima/commands/opsx/ # KunlunZhima OPSX command bridge(11 个)');
@@ -1397,11 +1648,6 @@ async function main() {
1397
1648
  if (selectedTools.includes('claude')) {
1398
1649
  console.log(' 🪝 .claude/hooks/ # Claude Code SDD Hook Pack(可选增强)');
1399
1650
  }
1400
- if (!layout.isWorkspace) {
1401
- console.log(' 📊 skywalk-sdd/ # SDD Telemetry 数据目录');
1402
- console.log(' 🧰 skywalk-sdd/git-hooks/ # Git hooks 质量门禁模板');
1403
- console.log(' 🧪 skywalk-sdd/ci/ # CI 兜底采集模板');
1404
- }
1405
1651
  console.log();
1406
1652
 
1407
1653
  // 统一的 skill 格式说明
@@ -1420,8 +1666,10 @@ async function main() {
1420
1666
 
1421
1667
  console.log();
1422
1668
  console.log('后续步骤:');
1423
- if (layout.isWorkspace && layout.specRepo) {
1424
- console.log(` 0. 文档类命令请以 spec 仓为 --project(例: --project=${layout.specRepo.name})`);
1669
+ if (layout.isWorkspace && specPackage) {
1670
+ console.log(` 0. 文档类命令请以包裹包为 --project(例: --project=${specPackage.name})`);
1671
+ } else if (specPackage) {
1672
+ console.log(` 0. 文档类命令请以包裹包为 --project(例: --project=${specPackage.name})`);
1425
1673
  }
1426
1674
  console.log(' 1. 激活 opsx-propose skill,输入变更名称开始创建文档');
1427
1675
  console.log(' 2. 按顺序执行 propose → spec → design → task');
@@ -1438,8 +1686,12 @@ async function main() {
1438
1686
  }
1439
1687
  console.log();
1440
1688
  console.log('📊 SDD Telemetry(嵌入在 skill 流程中,自动执行,无需配置):');
1441
- console.log(' - 度量数据自动采集到 skywalk-sdd/events/');
1442
- console.log(' - 运行 node skywalk-sdd/log.cjs metrics --project=. 查看四维度指标');
1689
+ if (specPackage) {
1690
+ console.log(` - 文档侧度量在 ${specPackage.name}/skywalk-sdd/events/`);
1691
+ } else {
1692
+ console.log(' - 度量数据自动采集到 skywalk-sdd/events/');
1693
+ }
1694
+ console.log(' - 运行 node skywalk-sdd/log.cjs metrics --project=<包裹包> 查看四维度指标');
1443
1695
  console.log();
1444
1696
 
1445
1697
  rl.close();
@@ -1458,6 +1710,10 @@ module.exports = {
1458
1710
  deploySddConfigYaml,
1459
1711
  deployDotSddYaml,
1460
1712
  deployMonoSddYaml,
1713
+ populateSpecPackage,
1714
+ writeSddSpecRootHint,
1715
+ linkSkywalkToSpecPackage,
1716
+ deploySkywalkForwarders,
1461
1717
  deployQualityGateTemplates,
1462
1718
  deployTelemetryDataDir,
1463
1719
  attachCodeRepoLite,