kld-sdd 2.6.9 → 2.6.10

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,7 +847,15 @@ 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)');
@@ -1065,32 +1063,98 @@ function deployDotSddYaml(cwd = process.cwd(), pkgPath = getPackagePath()) {
1065
1063
  }
1066
1064
 
1067
1065
  /**
1068
- * 单仓(openspec 与代码同仓)声明 layout: mono
1066
+ * 单仓声明 layout: mono,并记录仓内统一包裹目录相对路径。
1069
1067
  * commit-msg Hook 靠它跳过「spec 工作区干净」检查并只写 Spec-Change。
1070
1068
  */
1071
- function deployMonoSddYaml(cwd = process.cwd()) {
1069
+ function deployMonoSddYaml(cwd = process.cwd(), options = {}) {
1072
1070
  const target = path.join(cwd, '.sdd.yaml');
1071
+ const specRel = options.specPathRelative
1072
+ ? String(options.specPathRelative).replace(/\\/g, '/')
1073
+ : '';
1074
+
1073
1075
  if (fs.existsSync(target)) {
1074
1076
  const existing = fs.readFileSync(target, 'utf8');
1075
1077
  if (/^layout:\s*mono\s*$/m.test(existing)) {
1078
+ if (specRel && !new RegExp(`^spec_path:\\s*${specRel.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*$`, 'm').test(existing)) {
1079
+ // 已有 mono 但缺/错 spec_path:补写相对路径
1080
+ let next = existing.replace(/\s*$/, '');
1081
+ if (/^spec_path:\s*.+$/m.test(next)) {
1082
+ next = next.replace(/^spec_path:\s*.+$/m, `spec_path: ${specRel}`);
1083
+ } else {
1084
+ next = `${next}\nspec_path: ${specRel}`;
1085
+ }
1086
+ fs.writeFileSync(target, `${next}\n`, 'utf8');
1087
+ console.log(` ✓ 已更新 .sdd.yaml spec_path → ${specRel}`);
1088
+ return true;
1089
+ }
1076
1090
  console.log(' ✓ .sdd.yaml 已声明 layout: mono,跳过');
1077
1091
  return true;
1078
1092
  }
1079
- console.log('⚠️ .sdd.yaml 已存在但未声明 layout: mono;单仓请手动补一行 `layout: mono`');
1093
+ console.log('⚠️ .sdd.yaml 已存在但未声明 layout: mono;单仓请手动补 `layout: mono`');
1080
1094
  return false;
1081
1095
  }
1082
1096
  const lines = [
1083
- '# 单仓布局:openspec 文档与代码在同一个仓库',
1084
- '# commit-msg 只写 Spec-Change(spec 与代码同属一次 commit,无需 Spec-Revision 指针)',
1097
+ '# 单仓布局:业务代码与 SDD 包裹包在同一个 Git 仓库',
1098
+ '# SDD 内容统一在 spec_path 目录内(openspec / modules / sdd.config / 模版)',
1099
+ '# commit-msg 只写 Spec-Change(同仓一次 commit,无需 Spec-Revision 指针)',
1085
1100
  'version: 1',
1086
1101
  'layout: mono',
1087
- '',
1088
1102
  ];
1103
+ if (specRel) lines.push(`spec_path: ${specRel}`);
1104
+ lines.push('');
1089
1105
  fs.writeFileSync(target, lines.join('\n'), 'utf8');
1090
- console.log(`✅ 已创建 .sdd.yaml(layout: mono): ${target}`);
1106
+ console.log(`✅ 已创建 .sdd.yaml(layout: mono${specRel ? `, spec_path: ${specRel}` : ''}): ${target}`);
1091
1107
  return true;
1092
1108
  }
1093
1109
 
1110
+ /**
1111
+ * 向统一 SDD 包裹目录写入完整内容(多仓兄弟仓 / 单仓内部子目录共用)。
1112
+ * 不在此部署编辑器 skills——skills 只装工作目录或单仓 Git 根。
1113
+ */
1114
+ async function populateSpecPackage(specRoot, options = {}) {
1115
+ const root = path.resolve(specRoot);
1116
+ const skipOpenspec = Boolean(options.skipOpenspec);
1117
+ const asIndependentGit = Boolean(options.asIndependentGit);
1118
+ console.log(`📦 正在填充 SDD 包裹包: ${root}`);
1119
+
1120
+ if (!skipOpenspec) {
1121
+ const prev = process.cwd();
1122
+ try {
1123
+ process.chdir(root);
1124
+ // 包裹包内只建 openspec 目录结构,不把编辑器配置写进文档仓
1125
+ await runOpenspecInit([]);
1126
+ } finally {
1127
+ process.chdir(prev);
1128
+ }
1129
+ }
1130
+
1131
+ deployTelemetryDataDir(root);
1132
+ if (asIndependentGit || workspaceLayout.isGitRepo(root)) {
1133
+ deployQualityGateTemplates(root, { mode: 'full' });
1134
+ }
1135
+ deployModulesYaml(root);
1136
+ deploySddConfigYaml(root);
1137
+ copyTemplatesToProject(root);
1138
+ initGlobalOverview(root);
1139
+ deploySddGuideManual(root);
1140
+ console.log(`✅ SDD 包裹包已就绪: ${path.basename(root)}`);
1141
+ return { ok: true, path: root };
1142
+ }
1143
+
1144
+ /**
1145
+ * 在工作区 / Git 根写入 .sdd-spec-root,供 shell 与 skills 引用相对包裹包路径。
1146
+ */
1147
+ function writeSddSpecRootHint(anchorDir, specPackageAbs) {
1148
+ const anchor = path.resolve(anchorDir);
1149
+ const specAbs = path.resolve(specPackageAbs);
1150
+ const rel = path.relative(anchor, specAbs).replace(/\\/g, '/');
1151
+ if (!rel || rel.startsWith('..')) return null;
1152
+ const filePath = path.join(anchor, '.sdd-spec-root');
1153
+ fs.writeFileSync(filePath, `${rel}\n`, 'utf8');
1154
+ console.log(` ✓ 已写入 .sdd-spec-root → ${rel}`);
1155
+ return filePath;
1156
+ }
1157
+
1094
1158
  /**
1095
1159
  * 检测当前项目已初始化的 AI 编辑器
1096
1160
  */
@@ -1274,32 +1338,21 @@ async function main() {
1274
1338
  }
1275
1339
 
1276
1340
  const workspaceRoot = process.cwd();
1277
- const layout = workspaceLayout.detectWorkspaceLayout(workspaceRoot);
1341
+ let layout = workspaceLayout.detectWorkspaceLayout(workspaceRoot);
1342
+ let specPackage = layout.specRepo
1343
+ ? { name: layout.specRepo.name, abs: layout.specRepo.abs, created: false, isGit: true }
1344
+ : null;
1345
+
1278
1346
  if (layout.isWorkspace) {
1279
1347
  console.log('📂 检测到个人工作目录布局(子目录 Git 仓库)');
1280
- console.log(` Spec: ${layout.specRepo ? layout.specRepo.name : '(未发现)'}`);
1348
+ console.log(` Spec: ${specPackage ? specPackage.name : '(未发现,将自动创建包裹包)'}`);
1281
1349
  console.log(` 代码仓: ${layout.codeRepos.map((c) => c.name).join(', ') || '(无)'}`);
1282
- console.log(' → skills 只部署到本工作目录;代码仓仅轻量接入 Hook/关联');
1350
+ console.log(' → skills 只部署到本工作目录;SDD 文档统一进 *-sdd-specs 包裹包');
1283
1351
  console.log();
1284
1352
  }
1285
1353
 
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
1354
  if (!skipTemplate) {
1302
- // 2/3. skills 与编辑器产物:永远只装在当前工作目录一次
1355
+ // skills / 编辑器产物:永远只装在当前工作目录一次
1303
1356
  cleanupNativeOpenspecCommands(selectedTools);
1304
1357
  cleanupLegacyBundledOpsxSkills(selectedTools);
1305
1358
  deployOpsxSkills(selectedTools);
@@ -1307,49 +1360,93 @@ async function main() {
1307
1360
  deployProfileArtifacts(selectedTools, workspaceRoot, getPackagePath());
1308
1361
 
1309
1362
  if (layout.isWorkspace) {
1310
- // 工作目录本身保留一份 telemetry,供 skills 调用
1311
1363
  deployTelemetryDataDir(workspaceRoot);
1312
1364
 
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
- }
1365
+ if (!specPackage) {
1366
+ console.log('📦 未发现 spec 仓,正在创建统一 SDD 包裹包(独立 Git)...');
1367
+ specPackage = workspaceLayout.ensureSpecPackage(workspaceRoot, { initGit: true });
1368
+ console.log(` ✓ ${specPackage.created ? '已创建' : '已复用'}: ${specPackage.name}`);
1369
+ }
1327
1370
 
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');
1371
+ await populateSpecPackage(specPackage.abs, {
1372
+ skipOpenspec,
1373
+ asIndependentGit: true,
1374
+ });
1375
+ writeSddSpecRootHint(workspaceRoot, specPackage.abs);
1376
+
1377
+ // 重新探测,刷新 layout.specRepo / codeRepos
1378
+ layout = workspaceLayout.detectWorkspaceLayout(workspaceRoot);
1379
+ if (!layout.specRepo) {
1380
+ layout.specRepo = { name: specPackage.name, abs: specPackage.abs };
1381
+ }
1382
+
1383
+ console.log();
1384
+ console.log('🔗 正在为子代码仓做轻量接入(不安装 skills)...');
1385
+ for (const repo of layout.codeRepos) {
1386
+ attachCodeRepoLite(repo.abs, specPackage.abs);
1337
1387
  }
1388
+ const wsFile = workspaceLayout.writeWorkspaceFile(workspaceRoot, layout);
1389
+ console.log(`✅ 工作区清单: ${wsFile}`);
1338
1390
  } else {
1339
- // 单仓模式(旧行为):当前目录既是项目根
1391
+ // 单仓:Git 根只放代码 + skills + Hook;SDD 内容进统一包裹子目录
1392
+ const isGit = workspaceLayout.isGitRepo(workspaceRoot);
1340
1393
  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);
1394
+
1395
+ if (isGit) {
1396
+ console.log('📦 单仓模式:创建/复用仓内统一 SDD 包裹目录...');
1397
+ specPackage = workspaceLayout.ensureSpecPackage(workspaceRoot, { initGit: false });
1398
+ console.log(` ✓ ${specPackage.created ? '已创建' : '已复用'}: ${specPackage.name}`);
1399
+
1400
+ await populateSpecPackage(specPackage.abs, {
1401
+ skipOpenspec,
1402
+ asIndependentGit: false,
1403
+ });
1404
+ writeSddSpecRootHint(workspaceRoot, specPackage.abs);
1405
+
1406
+ deployMonoSddYaml(workspaceRoot, { specPathRelative: specPackage.name });
1407
+ // Hook 装在 Git 根;Trailer 读包裹目录里的 sdd.config / openspec
1408
+ deployQualityGateTemplates(workspaceRoot, { mode: 'commit-msg-only' });
1409
+ const linked = sddConfig.setSpecPath(workspaceRoot, specPackage.abs, {
1410
+ allowNestedPackage: true,
1411
+ });
1412
+ if (linked.ok) {
1413
+ console.log(` ✓ sdd.specPath = ${linked.path}`);
1414
+ } else {
1415
+ console.log(` ⚠️ 未能写入 sdd.specPath: ${linked.message}`);
1416
+ }
1347
1417
  } else {
1418
+ // 非 Git 目录:仍创建包裹包,便于随后 git init
1419
+ console.log('📦 当前目录不是 Git 仓:仍创建 SDD 包裹包(请稍后 git init)...');
1420
+ specPackage = workspaceLayout.ensureSpecPackage(workspaceRoot, { initGit: false });
1421
+ await populateSpecPackage(specPackage.abs, {
1422
+ skipOpenspec,
1423
+ asIndependentGit: false,
1424
+ });
1348
1425
  deployDotSddYaml(workspaceRoot);
1349
1426
  }
1350
- copyTemplatesToProject();
1351
- initGlobalOverview();
1352
- deploySddGuideManual(workspaceRoot);
1427
+ }
1428
+ } else if (!skipOpenspec) {
1429
+ // 仅 openspec、跳过模版时:仍尊重包裹包,避免在工作区根平铺
1430
+ if (layout.isWorkspace) {
1431
+ if (!specPackage) {
1432
+ specPackage = workspaceLayout.ensureSpecPackage(workspaceRoot, { initGit: true });
1433
+ }
1434
+ const prev = process.cwd();
1435
+ try {
1436
+ process.chdir(specPackage.abs);
1437
+ await runOpenspecInit([]);
1438
+ } finally {
1439
+ process.chdir(prev);
1440
+ }
1441
+ } else {
1442
+ specPackage = workspaceLayout.ensureSpecPackage(workspaceRoot, { initGit: false });
1443
+ const prev = process.cwd();
1444
+ try {
1445
+ process.chdir(specPackage.abs);
1446
+ await runOpenspecInit([]);
1447
+ } finally {
1448
+ process.chdir(prev);
1449
+ }
1353
1450
  }
1354
1451
  }
1355
1452
 
@@ -1369,8 +1466,9 @@ async function main() {
1369
1466
  console.log('工作区部署结果:');
1370
1467
  console.log(' 🎯 工作目录 .*/skills/opsx-*/ # skills 只装这一份');
1371
1468
  console.log(' 📋 .sdd-workspace.yaml # 子仓编排清单');
1372
- if (layout.specRepo) {
1373
- console.log(` 📄 ${layout.specRepo.name}/modules.yaml + openspec-templates`);
1469
+ if (specPackage) {
1470
+ console.log(` 📄 ${specPackage.name}/ # 统一 SDD 包裹包(独立 Git)`);
1471
+ console.log(' modules.yaml / sdd.config.yaml / openspec / openspec-templates / skywalk-sdd');
1374
1472
  }
1375
1473
  for (const repo of layout.codeRepos) {
1376
1474
  console.log(` 🔗 ${repo.name}/ ← commit-msg Hook + .sdd.yaml(无 skills)`);
@@ -1380,12 +1478,13 @@ async function main() {
1380
1478
  console.log(' kld-sdd sync-repos');
1381
1479
  console.log('Trailer 仍在各代码仓 git commit 时由 Hook 写入。');
1382
1480
  } 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(扁平一层)');
1481
+ console.log('单仓部署结果:');
1482
+ console.log(' 🎯 .*/skills/opsx-*/ # SDD skills(装在 Git 根)');
1483
+ if (specPackage) {
1484
+ console.log(` 📄 ${specPackage.name}/ # 统一 SDD 包裹包(仓内子目录)`);
1485
+ console.log(' modules.yaml / sdd.config.yaml / openspec / openspec-templates / skywalk-sdd');
1486
+ }
1487
+ console.log(' 🔗 .sdd.yaml (layout: mono) + commit-msg Hook');
1389
1488
  }
1390
1489
  if (selectedTools.includes('kunlunzhima')) {
1391
1490
  console.log(' 📎 .kunlunzhima/commands/opsx/ # KunlunZhima OPSX command bridge(11 个)');
@@ -1398,9 +1497,8 @@ async function main() {
1398
1497
  console.log(' 🪝 .claude/hooks/ # Claude Code SDD Hook Pack(可选增强)');
1399
1498
  }
1400
1499
  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 兜底采集模板');
1500
+ console.log(' 📊 skywalk-sdd/ # Hook 运行时 ontology(Git 根)');
1501
+ console.log(' 🧰 skywalk-sdd/git-hooks/ # commit-msg Trailer Hook');
1404
1502
  }
1405
1503
  console.log();
1406
1504
 
@@ -1420,8 +1518,10 @@ async function main() {
1420
1518
 
1421
1519
  console.log();
1422
1520
  console.log('后续步骤:');
1423
- if (layout.isWorkspace && layout.specRepo) {
1424
- console.log(` 0. 文档类命令请以 spec 仓为 --project(例: --project=${layout.specRepo.name})`);
1521
+ if (layout.isWorkspace && specPackage) {
1522
+ console.log(` 0. 文档类命令请以包裹包为 --project(例: --project=${specPackage.name})`);
1523
+ } else if (specPackage) {
1524
+ console.log(` 0. 文档类命令请以包裹包为 --project(例: --project=${specPackage.name})`);
1425
1525
  }
1426
1526
  console.log(' 1. 激活 opsx-propose skill,输入变更名称开始创建文档');
1427
1527
  console.log(' 2. 按顺序执行 propose → spec → design → task');
@@ -1438,8 +1538,12 @@ async function main() {
1438
1538
  }
1439
1539
  console.log();
1440
1540
  console.log('📊 SDD Telemetry(嵌入在 skill 流程中,自动执行,无需配置):');
1441
- console.log(' - 度量数据自动采集到 skywalk-sdd/events/');
1442
- console.log(' - 运行 node skywalk-sdd/log.cjs metrics --project=. 查看四维度指标');
1541
+ if (specPackage) {
1542
+ console.log(` - 文档侧度量在 ${specPackage.name}/skywalk-sdd/events/`);
1543
+ } else {
1544
+ console.log(' - 度量数据自动采集到 skywalk-sdd/events/');
1545
+ }
1546
+ console.log(' - 运行 node skywalk-sdd/log.cjs metrics --project=<包裹包> 查看四维度指标');
1443
1547
  console.log();
1444
1548
 
1445
1549
  rl.close();
@@ -1458,6 +1562,8 @@ module.exports = {
1458
1562
  deploySddConfigYaml,
1459
1563
  deployDotSddYaml,
1460
1564
  deployMonoSddYaml,
1565
+ populateSpecPackage,
1566
+ writeSddSpecRootHint,
1461
1567
  deployQualityGateTemplates,
1462
1568
  deployTelemetryDataDir,
1463
1569
  attachCodeRepoLite,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kld-sdd",
3
- "version": "2.6.9",
3
+ "version": "2.6.10",
4
4
  "description": "KLD SDD OpenSpec 项目初始化工具 - 一键部署 SDD skills",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -8,7 +8,7 @@
8
8
  "kld-sdd-init": "bin/kld-sdd-init.js"
9
9
  },
10
10
  "scripts": {
11
- "test": "node test/external-key.cjs && node test/change-key.cjs && node test/modules-and-sdd-config.cjs && node test/active-changes.cjs && node test/hook-layouts.cjs && node test/workspace-layout.cjs && node test/ontology-release-blockers.cjs && node test/ontology-semantic-core.cjs && node test/ontology-identity-versioning.cjs && node test/ontology-identity-continuity.cjs && node test/ontology-state-transaction.cjs && node test/ontology-process-concurrency.cjs && node test/ontology-observer-convergence.cjs && node test/ontology-working-runtime.cjs && node test/ontology-stage-materialization.cjs && node test/ontology-template-contract.cjs && node test/ontology-cli-archive.cjs && node test/archive-package-producer.cjs && node test/validate-skills-bundle.cjs && node test/tool-profiles.cjs && node test/settings-merge.cjs && node test/command-bridge.cjs && node test/codebuddy-hooks.cjs && node test/skill-content-contract.cjs && node test/init-agent-profiles.cjs"
11
+ "test": "node test/external-key.cjs && node test/change-key.cjs && node test/modules-and-sdd-config.cjs && node test/active-changes.cjs && node test/hook-layouts.cjs && node test/spec-package-init.cjs && node test/workspace-layout.cjs && node test/ontology-release-blockers.cjs && node test/ontology-semantic-core.cjs && node test/ontology-identity-versioning.cjs && node test/ontology-identity-continuity.cjs && node test/ontology-state-transaction.cjs && node test/ontology-process-concurrency.cjs && node test/ontology-observer-convergence.cjs && node test/ontology-working-runtime.cjs && node test/ontology-stage-materialization.cjs && node test/ontology-template-contract.cjs && node test/ontology-cli-archive.cjs && node test/archive-package-producer.cjs && node test/validate-skills-bundle.cjs && node test/tool-profiles.cjs && node test/settings-merge.cjs && node test/command-bridge.cjs && node test/codebuddy-hooks.cjs && node test/skill-content-contract.cjs && node test/init-agent-profiles.cjs"
12
12
  },
13
13
  "keywords": [
14
14
  "kld",
@@ -13,6 +13,7 @@
13
13
 
14
14
  const fs = require('fs');
15
15
  const path = require('path');
16
+ const { resolveProjectArg } = require('./ontology/resolve-spec-root.cjs');
16
17
  const crypto = require('crypto');
17
18
  const { execFileSync } = require('child_process');
18
19
 
@@ -73,11 +74,11 @@ function nowISO() {
73
74
  }
74
75
 
75
76
  function normalizeProjectRoot(projectRoot) {
76
- const rawProjectRoot = projectRoot || process.cwd();
77
+ const rawProjectRoot = projectRoot || '.';
77
78
  if (process.platform === 'win32' && /^[a-zA-Z]:[^\\/]/.test(String(rawProjectRoot))) {
78
79
  fail(`Windows 项目路径格式不安全: ${rawProjectRoot}。请使用 --project=.,或使用正斜杠路径如 D:/project/demo,或给路径加引号。`);
79
80
  }
80
- return path.resolve(rawProjectRoot);
81
+ return resolveProjectArg(rawProjectRoot, process.cwd());
81
82
  }
82
83
 
83
84
  function inferAgentType(args) {
@@ -17,6 +17,7 @@ const namingDiagnose = require('./naming-diagnose.cjs');
17
17
  const listChanges = require('./list-changes.cjs');
18
18
  const sddConfig = require('./sdd-config.cjs');
19
19
  const activeChanges = require('./active-changes.cjs');
20
+ const { resolveProjectArg } = require('./resolve-spec-root.cjs');
20
21
 
21
22
  function parseArgs(argv) {
22
23
  const result = { _: [] };
@@ -70,6 +71,7 @@ function showHelp() {
70
71
  node skywalk-sdd/ontology/cli.cjs active-change --register --change=<key> [--title=] [--summary=] [--module=]
71
72
  node skywalk-sdd/ontology/cli.cjs active-change --remove --change=<key>
72
73
  node skywalk-sdd/ontology/cli.cjs active-change --list [--json]
74
+ node skywalk-sdd/ontology/cli.cjs spec-root [--project=.] [--json]
73
75
  node skywalk-sdd/ontology/cli.cjs reconcile --project=. --change=<name> [--profile=...]
74
76
  node skywalk-sdd/ontology/cli.cjs check --project=. --change=<name> [--profile=...]
75
77
  node skywalk-sdd/ontology/cli.cjs status --project=. --change=<name>
@@ -79,6 +81,10 @@ function showHelp() {
79
81
  observe 只负责采集和同步;check/archive 前仍必须执行 reconcile。`);
80
82
  }
81
83
 
84
+ function resolveCliProject(args, options = {}) {
85
+ return resolveProjectArg(args.project || '.', process.cwd(), options);
86
+ }
87
+
82
88
  function main(argv = process.argv.slice(2)) {
83
89
  const args = parseArgs(argv);
84
90
  const command = args._[0];
@@ -109,7 +115,7 @@ function main(argv = process.argv.slice(2)) {
109
115
  }
110
116
  if (command === 'change-key') {
111
117
  if (args.generate || args._[1] === 'generate') {
112
- const projectRoot = path.resolve(args.project || '.');
118
+ const projectRoot = resolveCliProject(args);
113
119
  const registry = modules.loadModulesYaml(projectRoot);
114
120
  const existing = listChanges.listActiveChanges(projectRoot);
115
121
  const built = changeKey.buildChangeKey(args.module, args.slug, {
@@ -123,7 +129,7 @@ function main(argv = process.argv.slice(2)) {
123
129
  }
124
130
  const key = args.validate || args._[1];
125
131
  if (!key) throw new Error('用法: change-key --validate <key> | --generate --module= --slug=');
126
- const projectRoot = path.resolve(args.project || '.');
132
+ const projectRoot = resolveCliProject(args);
127
133
  const registry = modules.loadModulesYaml(projectRoot);
128
134
  const result = changeKey.validate(key, {
129
135
  registeredModules: registry.ok ? registry.codes : null,
@@ -140,7 +146,7 @@ function main(argv = process.argv.slice(2)) {
140
146
  return;
141
147
  }
142
148
  if (command === 'modules') {
143
- const projectRoot = path.resolve(args.project || '.');
149
+ const projectRoot = resolveCliProject(args);
144
150
  const result = modules.loadModulesYaml(projectRoot);
145
151
  console.log(JSON.stringify({
146
152
  ok: result.ok,
@@ -170,7 +176,7 @@ function main(argv = process.argv.slice(2)) {
170
176
  return;
171
177
  }
172
178
  if (command === 'list-changes') {
173
- const projectRoot = path.resolve(args.project || '.');
179
+ const projectRoot = resolveCliProject(args);
174
180
  const result = listChanges.groupChanges(projectRoot);
175
181
  if (args.json) {
176
182
  console.log(JSON.stringify(result, null, 2));
@@ -179,8 +185,29 @@ function main(argv = process.argv.slice(2)) {
179
185
  }
180
186
  return;
181
187
  }
188
+ if (command === 'spec-root') {
189
+ const start = path.resolve(args.project || '.');
190
+ const { resolveSpecProjectRoot, isSpecPackageRoot } = require('./resolve-spec-root.cjs');
191
+ const specRoot = resolveSpecProjectRoot(start);
192
+ const payload = {
193
+ start,
194
+ specRoot,
195
+ resolved: specRoot !== start || isSpecPackageRoot(start),
196
+ openspecShim: 'node skywalk-sdd/openspec-shim.cjs <openspec-args>',
197
+ };
198
+ if (args.json) {
199
+ console.log(JSON.stringify(payload, null, 2));
200
+ } else {
201
+ console.log(`start: ${payload.start}`);
202
+ console.log(`specRoot: ${payload.specRoot}`);
203
+ console.log(`resolved: ${payload.resolved}`);
204
+ console.log(`openspec: cd "${payload.specRoot}" && openspec list`);
205
+ console.log(`shim: ${payload.openspecShim}`);
206
+ }
207
+ return;
208
+ }
182
209
  if (command === 'link-spec') {
183
- const projectRoot = path.resolve(args.project || '.');
210
+ const projectRoot = resolveCliProject(args, { forCodeRepo: true });
184
211
  const specPath = args.path || args._[1];
185
212
  if (!specPath) throw new Error('用法: link-spec --path=<spec-clone> [--project=.]');
186
213
  const result = sddConfig.setSpecPath(projectRoot, specPath);
@@ -189,7 +216,7 @@ function main(argv = process.argv.slice(2)) {
189
216
  return;
190
217
  }
191
218
  if (command === 'active-change') {
192
- const specRoot = path.resolve(args.project || '.');
219
+ const specRoot = resolveCliProject(args);
193
220
  if (args.list || args._[1] === 'list') {
194
221
  const config = activeChanges.loadSddConfig(specRoot);
195
222
  if (args.json) {
@@ -227,7 +254,7 @@ function main(argv = process.argv.slice(2)) {
227
254
  if (!result.ok) process.exitCode = 1;
228
255
  return;
229
256
  }
230
- const projectRoot = path.resolve(args.project || '.');
257
+ const projectRoot = resolveCliProject(args);
231
258
  const changeName = args.change;
232
259
  if (!changeName) throw new Error('缺少 --change 参数');
233
260
  const profile = args.profile || 'auto';
@@ -57,6 +57,7 @@ function readProposalFrontmatter(changeDir) {
57
57
  }
58
58
 
59
59
  function detectEntryMode(projectRoot) {
60
+ if (sddConfig.isMonoLayout(projectRoot)) return 'mono-repo';
60
61
  const hasModules = fs.existsSync(path.join(projectRoot, 'modules.yaml'));
61
62
  const hasOpenspecChanges = fs.existsSync(path.join(projectRoot, 'openspec', 'changes'));
62
63
  const hasSddYaml = fs.existsSync(path.join(projectRoot, '.sdd.yaml'));
@@ -281,40 +282,94 @@ function diagnoseSpecRepo(projectRoot, changeName) {
281
282
  ...diagnoseActiveChanges(projectRoot, changeName),
282
283
  });
283
284
 
284
- // 单仓(openspec 与代码同仓):Hook 装在本仓,应就地检查
285
- if (isMonoRepoLayout(projectRoot)) {
285
+ checks.push({
286
+ id: 'code-repo-association',
287
+ status: STATUS.SKIP,
288
+ message: '当前位于 spec 仓库:代码仓 .sdd.yaml / Hook 请在各代码仓库或工作目录诊断',
289
+ });
290
+
291
+ return {
292
+ mode: 'spec-repo',
293
+ checks,
294
+ summary: summarize(checks),
295
+ };
296
+ }
297
+
298
+ /**
299
+ * 单仓:Git 根有 layout: mono;SDD 内容在仓内统一包裹目录。
300
+ */
301
+ function diagnoseMonoRepo(gitRoot, changeName) {
302
+ const root = path.resolve(gitRoot);
303
+ const specRoot = sddConfig.resolveMonoSpecRoot(root);
304
+ const checks = [];
305
+
306
+ checks.push({
307
+ id: 'spec-git',
308
+ status: workspaceLayout.isGitRepo(root) ? STATUS.PASS : STATUS.FAIL,
309
+ message: workspaceLayout.isGitRepo(root)
310
+ ? '单仓 Git 根可用'
311
+ : '单仓目录不是 Git 仓库',
312
+ });
313
+
314
+ const remote = sddConfig.gitRemoteUrl(root);
315
+ checks.push({
316
+ id: 'spec-remote',
317
+ status: remote ? STATUS.PASS : STATUS.NEEDS_INPUT,
318
+ message: remote ? `remote 可识别: ${remote}` : '无法识别 git remote',
319
+ remote,
320
+ });
321
+
322
+ const registry = modules.loadModulesYaml(specRoot);
323
+ checks.push({
324
+ id: 'modules-yaml',
325
+ status: registry.ok ? STATUS.PASS : (registry.code === modules.CODES.MISSING ? STATUS.NEEDS_INPUT : STATUS.FAIL),
326
+ message: registry.ok
327
+ ? `modules.yaml 合法(${registry.codes.size} 个模块,路径 ${path.relative(root, specRoot) || '.'})`
328
+ : registry.message,
329
+ registry,
330
+ });
331
+
332
+ if (changeName) {
333
+ const naming = diagnoseChangeNaming(specRoot, changeName, registry.ok ? registry : null);
286
334
  checks.push({
287
- id: 'commit-hook',
288
- ...checkHookInstalled(projectRoot),
335
+ id: 'change-naming',
336
+ status: naming.status,
337
+ message: naming.message,
338
+ detail: naming,
289
339
  });
340
+ } else {
290
341
  checks.push({
291
- id: 'code-repo-association',
292
- status: STATUS.PASS,
293
- message: '单仓布局(.sdd.yaml layout: mono):commit 写 Spec-Change,不写 Spec-Revision',
342
+ id: 'change-naming',
343
+ status: STATUS.SKIP,
344
+ message: '未指定 --change,跳过命名一致性检查',
294
345
  });
295
- return {
296
- mode: 'mono-repo',
297
- checks,
298
- summary: summarize(checks),
299
- };
300
346
  }
301
347
 
348
+ checks.push({
349
+ id: 'active-changes',
350
+ ...diagnoseActiveChanges(specRoot, changeName),
351
+ });
352
+
353
+ checks.push({
354
+ id: 'commit-hook',
355
+ ...checkHookInstalled(root),
356
+ });
302
357
  checks.push({
303
358
  id: 'code-repo-association',
304
- status: STATUS.SKIP,
305
- message: '当前位于 spec 仓库:代码仓 .sdd.yaml / Hook 请在各代码仓库或工作目录诊断',
359
+ status: STATUS.PASS,
360
+ message: `单仓布局(layout: mono,包裹包 ${path.relative(root, specRoot) || '.'}):commit Spec-Change,不写 Spec-Revision`,
306
361
  });
307
362
 
308
363
  return {
309
- mode: 'spec-repo',
364
+ mode: 'mono-repo',
310
365
  checks,
311
366
  summary: summarize(checks),
367
+ specRoot,
312
368
  };
313
369
  }
314
370
 
315
371
  /**
316
- * 单仓:openspec 与代码同仓,由 .sdd.yaml 的 layout: mono 显式声明。
317
- * 不用启发式,否则独立的 spec 仓 clone 会被误判成单仓。
372
+ * 单仓:由 .sdd.yaml 的 layout: mono 显式声明。
318
373
  */
319
374
  function isMonoRepoLayout(projectRoot) {
320
375
  return sddConfig.isMonoLayout(path.resolve(projectRoot));
@@ -532,6 +587,10 @@ function diagnose(projectRoot, options = {}) {
532
587
  };
533
588
  }
534
589
 
590
+ if (mode === 'mono-repo' || isMonoRepoLayout(root)) {
591
+ return diagnoseMonoRepo(root, options.change);
592
+ }
593
+
535
594
  if (mode === 'code-repo') {
536
595
  return diagnoseCodeRepo(root);
537
596
  }
@@ -586,6 +645,7 @@ module.exports = {
586
645
  diagnoseChangeNaming,
587
646
  diagnoseActiveChanges,
588
647
  diagnoseWorkspaceRepos,
648
+ diagnoseMonoRepo,
589
649
  isMonoRepoLayout,
590
650
  diagnose,
591
651
  diagnoseTrailer,
@@ -0,0 +1,102 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const sddConfig = require('./sdd-config.cjs');
6
+ const workspaceLayout = require('./workspace-layout.cjs');
7
+
8
+ function isSpecPackageRoot(dir) {
9
+ const root = path.resolve(dir);
10
+ return (
11
+ fs.existsSync(path.join(root, 'openspec', 'changes'))
12
+ || fs.existsSync(path.join(root, 'modules.yaml'))
13
+ || fs.existsSync(path.join(root, 'sdd.config.yaml'))
14
+ );
15
+ }
16
+
17
+ /**
18
+ * 解析 SDD 文档根(*-sdd-specs 包裹包或旧平铺 spec 根)。
19
+ * openspec CLI 只认 cwd 下的 openspec/;本函数供 log.cjs / ontology cli 自动定位包裹包。
20
+ *
21
+ * 解析顺序:
22
+ * 1. start 本身已是 spec 包根
23
+ * 2. KLD_SDD_SPEC_ROOT(绝对或相对 start)
24
+ * 3. 单仓 layout: mono → spec_path / 子目录 *-sdd-specs
25
+ * 4. 工作区 layout → specRepo
26
+ * 5. start 下嵌套包裹包
27
+ * 6. 代码仓 git config sdd.specPath(含仓内 nested 包裹)
28
+ * 7. 向上最多 3 层找 mono / 嵌套包(Git 根、工作区根常见)
29
+ * 8. 回退 start
30
+ */
31
+ function resolveSpecProjectRoot(startDir = process.cwd(), options = {}) {
32
+ const start = path.resolve(startDir || process.cwd());
33
+ if (options.skipResolve || options.forCodeRepo) return start;
34
+ if (isSpecPackageRoot(start)) return start;
35
+
36
+ const envRoot = process.env.KLD_SDD_SPEC_ROOT && String(process.env.KLD_SDD_SPEC_ROOT).trim();
37
+ if (envRoot) {
38
+ const candidate = path.isAbsolute(envRoot) ? envRoot : path.resolve(start, envRoot);
39
+ if (fs.existsSync(candidate)) return candidate;
40
+ }
41
+
42
+ if (sddConfig.isMonoLayout(start)) {
43
+ const monoRoot = sddConfig.resolveMonoSpecRoot(start);
44
+ if (fs.existsSync(monoRoot)) return monoRoot;
45
+ }
46
+
47
+ const layout = workspaceLayout.detectWorkspaceLayout(start);
48
+ if (layout.specRepo && fs.existsSync(layout.specRepo.abs)) {
49
+ return layout.specRepo.abs;
50
+ }
51
+
52
+ const nested = workspaceLayout.findNestedSpecPackage(start);
53
+ if (nested) return nested.abs;
54
+
55
+ const specPathResult = sddConfig.resolveSpecPath(start);
56
+ if (specPathResult.ok) {
57
+ const valid = sddConfig.validateSpecPath(specPathResult.path, { allowNestedPackage: true });
58
+ if (valid.ok) return valid.path;
59
+ }
60
+
61
+ let dir = start;
62
+ for (let depth = 0; depth < 3; depth += 1) {
63
+ const parent = path.dirname(dir);
64
+ if (parent === dir) break;
65
+ if (sddConfig.isMonoLayout(parent)) {
66
+ const monoRoot = sddConfig.resolveMonoSpecRoot(parent);
67
+ if (fs.existsSync(monoRoot)) return monoRoot;
68
+ }
69
+ const parentNested = workspaceLayout.findNestedSpecPackage(parent);
70
+ if (parentNested) return parentNested.abs;
71
+ const parentLayout = workspaceLayout.detectWorkspaceLayout(parent);
72
+ if (parentLayout.specRepo && fs.existsSync(parentLayout.specRepo.abs)) {
73
+ return parentLayout.specRepo.abs;
74
+ }
75
+ dir = parent;
76
+ }
77
+
78
+ return start;
79
+ }
80
+
81
+ /**
82
+ * CLI / skills 用:解析 --project 参数。
83
+ * '.' 或未显式绝对 spec 根时走 resolve;link-spec 等代码仓命令传 forCodeRepo:true。
84
+ */
85
+ function resolveProjectArg(projectArg, startDir = process.cwd(), options = {}) {
86
+ const raw = projectArg == null || projectArg === '' ? '.' : String(projectArg);
87
+ const resolved = path.resolve(startDir, raw);
88
+ if (options.forCodeRepo) return resolved;
89
+ if (raw !== '.' && raw !== './' && path.isAbsolute(raw)) {
90
+ return isSpecPackageRoot(resolved) ? resolved : resolveSpecProjectRoot(resolved, options);
91
+ }
92
+ if (raw !== '.' && raw !== './') {
93
+ if (isSpecPackageRoot(resolved)) return resolved;
94
+ }
95
+ return resolveSpecProjectRoot(startDir, options);
96
+ }
97
+
98
+ module.exports = {
99
+ isSpecPackageRoot,
100
+ resolveSpecProjectRoot,
101
+ resolveProjectArg,
102
+ };
@@ -35,15 +35,17 @@ function stripYamlQuotes(value) {
35
35
  * version: 1
36
36
  * spec_repository: git@host:group/repo.git
37
37
  *
38
- * 单仓:openspec 与代码同仓,无外部 spec 可指
38
+ * 单仓:同一 Git 仓内用统一包裹目录存放 openspec / 配置
39
39
  * version: 1
40
40
  * layout: mono
41
+ * spec_path: myapp-sdd-specs
41
42
  */
42
43
  function parseSddYaml(text) {
43
44
  const lines = String(text || '').split(/\r?\n/);
44
45
  let version = null;
45
46
  let specRepository = null;
46
47
  let layout = null;
48
+ let specPathRel = null;
47
49
 
48
50
  for (let index = 0; index < lines.length; index += 1) {
49
51
  const raw = lines[index];
@@ -76,6 +78,12 @@ function parseSddYaml(text) {
76
78
  continue;
77
79
  }
78
80
 
81
+ const pathMatch = /^spec_path:\s*(.+?)\s*$/.exec(line);
82
+ if (pathMatch) {
83
+ specPathRel = stripYamlQuotes(pathMatch[1]);
84
+ continue;
85
+ }
86
+
79
87
  return {
80
88
  ok: false,
81
89
  code: CODES.FORMAT_INVALID,
@@ -108,6 +116,7 @@ function parseSddYaml(text) {
108
116
  version,
109
117
  layout: resolvedLayout,
110
118
  spec_repository: specRepository || '',
119
+ spec_path: specPathRel || '',
111
120
  };
112
121
  }
113
122
 
@@ -132,6 +141,29 @@ function isMonoLayout(repoRoot) {
132
141
  return loaded.ok && loaded.layout === LAYOUTS.MONO;
133
142
  }
134
143
 
144
+ /**
145
+ * 单仓 SDD 包裹根:优先 .sdd.yaml spec_path → 子目录 *-sdd-specs → 兼容旧平铺根目录。
146
+ */
147
+ function resolveMonoSpecRoot(repoRoot) {
148
+ const root = path.resolve(repoRoot);
149
+ const loaded = loadSddYaml(root);
150
+ if (loaded.ok && loaded.spec_path) {
151
+ return path.resolve(root, loaded.spec_path);
152
+ }
153
+ const workspaceLayout = require('./workspace-layout.cjs');
154
+ const nested = workspaceLayout.findNestedSpecPackage(root);
155
+ if (nested) return nested.abs;
156
+ return root;
157
+ }
158
+
159
+ function looksLikeSpecPackageContent(dir) {
160
+ return (
161
+ fs.existsSync(path.join(dir, 'modules.yaml'))
162
+ || fs.existsSync(path.join(dir, 'sdd.config.yaml'))
163
+ || fs.existsSync(path.join(dir, 'openspec', 'changes'))
164
+ );
165
+ }
166
+
135
167
  function gitConfig(cwd, key) {
136
168
  try {
137
169
  return execFileSync('git', ['config', '--local', '--get', key], {
@@ -203,7 +235,7 @@ function resolveSpecPath(codeRepoRoot, env = process.env) {
203
235
  };
204
236
  }
205
237
 
206
- function validateSpecPath(specPath) {
238
+ function validateSpecPath(specPath, options = {}) {
207
239
  if (!specPath || !fs.existsSync(specPath)) {
208
240
  return {
209
241
  ok: false,
@@ -211,25 +243,37 @@ function validateSpecPath(specPath) {
211
243
  message: `spec 路径不存在: ${specPath}`,
212
244
  };
213
245
  }
214
- if (!fs.existsSync(path.join(specPath, '.git'))) {
215
- return {
216
- ok: false,
217
- code: CODES.SPEC_PATH_INVALID,
218
- message: `spec 路径不是 Git 仓库: ${specPath}`,
219
- };
246
+ if (fs.existsSync(path.join(specPath, '.git'))) {
247
+ return { ok: true, path: specPath, nested: false };
248
+ }
249
+ // 单仓内部包裹包:不是独立 Git,但内容齐全且落在某个 Git 仓内
250
+ if (options.allowNestedPackage && looksLikeSpecPackageContent(specPath)) {
251
+ return { ok: true, path: specPath, nested: true };
220
252
  }
221
- return { ok: true, path: specPath };
253
+ return {
254
+ ok: false,
255
+ code: CODES.SPEC_PATH_INVALID,
256
+ message: `spec 路径不是 Git 仓库: ${specPath}`,
257
+ };
222
258
  }
223
259
 
224
- function setSpecPath(codeRepoRoot, specPath) {
260
+ function setSpecPath(codeRepoRoot, specPath, options = {}) {
225
261
  const absolute = path.resolve(specPath);
226
- const valid = validateSpecPath(absolute);
262
+ const valid = validateSpecPath(absolute, options);
227
263
  if (!valid.ok) return valid;
228
- execFileSync('git', ['config', '--local', 'sdd.specPath', absolute], {
229
- cwd: codeRepoRoot,
230
- stdio: ['ignore', 'pipe', 'pipe'],
231
- });
232
- return { ok: true, path: absolute };
264
+ try {
265
+ execFileSync('git', ['config', '--local', 'sdd.specPath', absolute], {
266
+ cwd: codeRepoRoot,
267
+ stdio: ['ignore', 'pipe', 'pipe'],
268
+ });
269
+ } catch (error) {
270
+ return {
271
+ ok: false,
272
+ code: CODES.SPEC_PATH_INVALID,
273
+ message: `无法写入 git config sdd.specPath(请确认目录是真实 Git 仓库): ${error.message}`,
274
+ };
275
+ }
276
+ return { ok: true, path: absolute, nested: Boolean(valid.nested) };
233
277
  }
234
278
 
235
279
  function resolveCodeRepoContext(codeRepoRoot, env = process.env) {
@@ -320,6 +364,8 @@ module.exports = {
320
364
  LAYOUTS,
321
365
  parseSddYaml,
322
366
  isMonoLayout,
367
+ resolveMonoSpecRoot,
368
+ looksLikeSpecPackageContent,
323
369
  loadSddYaml,
324
370
  gitConfig,
325
371
  gitRemoteUrl,
@@ -181,14 +181,95 @@ function findNewCodeRepos(workspaceRoot) {
181
181
  };
182
182
  }
183
183
 
184
+ /**
185
+ * 推导统一 SDD 包裹目录名:`<parentBasename>-sdd-specs`
186
+ */
187
+ function deriveSpecPackageName(parentDir) {
188
+ const base = path.basename(path.resolve(parentDir)).toLowerCase();
189
+ if (!base || base === '.' || base === '/' || base === '\\') return 'project-sdd-specs';
190
+ if (base.endsWith('sdd-specs')) return path.basename(path.resolve(parentDir));
191
+ return `${path.basename(path.resolve(parentDir))}-sdd-specs`;
192
+ }
193
+
194
+ function listChildDirectories(parentDir) {
195
+ const root = path.resolve(parentDir);
196
+ if (!fs.existsSync(root)) return [];
197
+ return fs.readdirSync(root)
198
+ .filter((name) => !name.startsWith('.') && !IGNORE_DIR_NAMES.has(name))
199
+ .map((name) => ({ name, abs: path.join(root, name) }))
200
+ .filter((entry) => {
201
+ try {
202
+ return fs.statSync(entry.abs).isDirectory();
203
+ } catch {
204
+ return false;
205
+ }
206
+ })
207
+ .sort((a, b) => a.name.localeCompare(b.name));
208
+ }
209
+
210
+ /**
211
+ * 在父目录下查找已有的 SDD 包裹包(不必是独立 Git)。
212
+ * 优先:modules.yaml → 目录名 *-sdd-specs → openspec/changes。
213
+ */
214
+ function findNestedSpecPackage(parentDir) {
215
+ const children = listChildDirectories(parentDir);
216
+ const candidates = children.filter((c) => looksLikeSpecRepo(c.abs));
217
+ if (candidates.length === 0) return null;
218
+ return (
219
+ candidates.find((c) => fs.existsSync(path.join(c.abs, 'modules.yaml')))
220
+ || candidates.find((c) => /-sdd-specs$/i.test(c.name))
221
+ || candidates[0]
222
+ );
223
+ }
224
+
225
+ /**
226
+ * 确保父目录下存在统一的 SDD 包裹包目录。
227
+ * @param {{ name?: string, initGit?: boolean }} options
228
+ * initGit=true:多仓兄弟目录,包裹包本身是独立 Git 仓
229
+ * initGit=false:单仓内部子目录,共用外层 Git
230
+ */
231
+ function ensureSpecPackage(parentDir, options = {}) {
232
+ const { execFileSync } = require('child_process');
233
+ const root = path.resolve(parentDir);
234
+ const existing = findNestedSpecPackage(root);
235
+ if (existing) {
236
+ return {
237
+ name: existing.name,
238
+ abs: existing.abs,
239
+ created: false,
240
+ isGit: isGitRepo(existing.abs),
241
+ };
242
+ }
243
+
244
+ const name = options.name || deriveSpecPackageName(root);
245
+ const abs = path.join(root, name);
246
+ const existed = fs.existsSync(abs);
247
+ fs.mkdirSync(abs, { recursive: true });
248
+
249
+ let isGit = isGitRepo(abs);
250
+ if (options.initGit && !isGit) {
251
+ execFileSync('git', ['init'], {
252
+ cwd: abs,
253
+ stdio: ['ignore', 'pipe', 'pipe'],
254
+ });
255
+ isGit = true;
256
+ }
257
+
258
+ return { name, abs, created: !existed, isGit };
259
+ }
260
+
184
261
  module.exports = {
185
262
  WORKSPACE_FILE,
186
263
  isGitRepo,
187
264
  looksLikeSpecRepo,
188
265
  listChildGitRepos,
266
+ listChildDirectories,
189
267
  detectWorkspaceLayout,
190
268
  parseWorkspaceYaml,
191
269
  loadWorkspaceFile,
192
270
  writeWorkspaceFile,
193
271
  findNewCodeRepos,
272
+ deriveSpecPackageName,
273
+ findNestedSpecPackage,
274
+ ensureSpecPackage,
194
275
  };
@@ -60,7 +60,7 @@ function resolveLayout(repoRoot, env = process.env) {
60
60
  return { mode: 'unlinked' };
61
61
  }
62
62
  if (sddConfig.isMonoLayout(repoRoot)) {
63
- return { mode: 'mono', specPath: repoRoot };
63
+ return { mode: 'mono', specPath: sddConfig.resolveMonoSpecRoot(repoRoot) };
64
64
  }
65
65
 
66
66
  const context = sddConfig.resolveCodeRepoContext(repoRoot, env);
@@ -70,9 +70,14 @@ function resolveLayout(repoRoot, env = process.env) {
70
70
  if (context.code === sddConfig.CODES.SPEC_PATH_INVALID) {
71
71
  fail(context.message);
72
72
  }
73
- // specPath 指回本仓:等同单仓,不能按外部 spec 校验干净度
74
- if (path.resolve(context.specPath) === path.resolve(repoRoot)) {
75
- return { mode: 'mono', specPath: repoRoot, context };
73
+ // specPath 指回本仓根:等同单仓(兼容旧平铺);若指向仓内包裹包仍按 mono
74
+ const resolvedSpec = path.resolve(context.specPath);
75
+ const resolvedRoot = path.resolve(repoRoot);
76
+ if (resolvedSpec === resolvedRoot) {
77
+ return { mode: 'mono', specPath: sddConfig.resolveMonoSpecRoot(repoRoot), context };
78
+ }
79
+ if (resolvedSpec.startsWith(`${resolvedRoot}${path.sep}`)) {
80
+ return { mode: 'mono', specPath: resolvedSpec, context };
76
81
  }
77
82
  return { mode: 'multi', specPath: context.specPath, context };
78
83
  }
@@ -27,7 +27,8 @@ allowed-tools:
27
27
  > - ⛔ **隔离红线**:绝对禁止加载同级其他 Capability 的文档
28
28
 
29
29
  > **🖥️ 跨平台执行规则**
30
- > - 先确认当前终端工作目录是项目根目录;若不是,先 `cd` 到项目根目录。
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 到包裹包)。
31
32
  > - Telemetry 命令默认使用 `--project=.`,兼容 Windows、macOS、Linux。
32
33
  > - ${SHELL_GUIDANCE}
33
34
  > - 不要省略 `--source=opsx-command` 与 `--session-id=<会话ID>`。
@@ -20,7 +20,9 @@ allowed-tools:
20
20
  > **硬依赖(收尾入库)**:zip 生成后的上传依赖同级已部署的 **`opsx-kb-ingest`**。进入 §5.5 前必须先 `Read` 该技能的 `SKILL.md` 并完成其 Session 启动。入库成功后,`Read` `opsx-ontology-query/phase-3-postchange.md` 验证版本生效、AC 保留、关系完整性。缺失则提示用户重新 `kld-sdd-init`,**不要**自造另一套入库协议。
21
21
 
22
22
  > **跨平台执行规则**
23
- > - 先确认当前终端工作目录是项目根目录;若不是,先 `cd` 到项目根目录。
23
+ > - **SDD 文档根** = `*-sdd-specs` 包裹包(含 `openspec/`、`modules.yaml`),不是 Git 根或工作区根。
24
+ > - `openspec` 命令:优先 `node skywalk-sdd/openspec-shim.cjs list`(自动 cd 到包裹包),或先 `cd` 到包裹包再执行。
25
+ > - `skywalk-sdd/log.cjs` / `ontology/cli.cjs`:`--project=.` 在 Git 根/工作区根会自动解析;也可 `node skywalk-sdd/ontology/cli.cjs spec-root` 查看路径。
24
26
  > - Telemetry 命令默认使用 `--project=.`,兼容 Windows、macOS、Linux。
25
27
  > - 不要裸写 Windows 反斜杠绝对路径;如必须使用绝对路径,请加引号或改成正斜杠。
26
28
  > - 不要省略 `--source=opsx-command` 和 `--session-id=<会话ID>`。
@@ -32,7 +32,9 @@ allowed-tools:
32
32
 
33
33
 
34
34
  > **🖥️ 跨平台执行规则**
35
- > - 先确认当前终端工作目录是项目根目录;若不是,先 `cd` 到项目根目录。
35
+ > - **SDD 文档根** = `*-sdd-specs` 包裹包(含 `openspec/`、`modules.yaml`),不是 Git 根或工作区根。
36
+ > - `openspec` 命令:优先 `node skywalk-sdd/openspec-shim.cjs list`(自动 cd 到包裹包),或先 `cd` 到包裹包再执行。
37
+ > - `skywalk-sdd/log.cjs` / `ontology/cli.cjs`:`--project=.` 在 Git 根/工作区根会自动解析;也可 `node skywalk-sdd/ontology/cli.cjs spec-root` 查看路径。
36
38
  > - Telemetry 命令默认使用 `--project=.`,兼容 Windows、macOS、Linux。
37
39
  > - ${SHELL_GUIDANCE}
38
40
  > - 不要省略 `--source=opsx-command` 与 `--session-id=<会话ID>`。
@@ -46,7 +46,9 @@ allowed-tools:
46
46
  > - ⛔ **隔离红线**:绝对禁止跨目录读取同级其他 Capability 的 spec 或 design(Full 模式)
47
47
 
48
48
  > **🖥️ 跨平台执行规则**
49
- > - 先确认当前终端工作目录是项目根目录;若不是,先 `cd` 到项目根目录。
49
+ > - **SDD 文档根** = `*-sdd-specs` 包裹包(含 `openspec/`、`modules.yaml`),不是 Git 根或工作区根。
50
+ > - `openspec` 命令:优先 `node skywalk-sdd/openspec-shim.cjs list`(自动 cd 到包裹包),或先 `cd` 到包裹包再执行。
51
+ > - `skywalk-sdd/log.cjs` / `ontology/cli.cjs`:`--project=.` 在 Git 根/工作区根会自动解析;也可 `node skywalk-sdd/ontology/cli.cjs spec-root` 查看路径。
50
52
  > - Telemetry 命令默认使用 `--project=.`,兼容 Windows、macOS、Linux。
51
53
  > - ${SHELL_GUIDANCE}
52
54
  > - 不要省略 `--source=opsx-command` 与 `--session-id=<会话ID>`。
@@ -16,7 +16,9 @@ allowed-tools:
16
16
 
17
17
 
18
18
  > **🖥️ 跨平台执行规则**
19
- > - 先确认当前终端工作目录是项目根目录;若不是,先 `cd` 到项目根目录。
19
+ > - **SDD 文档根** = `*-sdd-specs` 包裹包(含 `openspec/`、`modules.yaml`),不是 Git 根或工作区根。
20
+ > - `openspec` 命令:优先 `node skywalk-sdd/openspec-shim.cjs list`(自动 cd 到包裹包),或先 `cd` 到包裹包再执行。
21
+ > - `skywalk-sdd/log.cjs` / `ontology/cli.cjs`:`--project=.` 在 Git 根/工作区根会自动解析;也可 `node skywalk-sdd/ontology/cli.cjs spec-root` 查看路径。
20
22
  > - Telemetry 命令默认使用 `--project=.`,兼容 Windows、macOS、Linux。
21
23
  > - ${SHELL_GUIDANCE}
22
24
  > - 不要省略 `--source=opsx-command` 与 `--session-id=<会话ID>`。
@@ -34,7 +34,9 @@ allowed-tools:
34
34
  > 阶段边界自检见 `./checklist.md`「阶段边界⛔」。
35
35
 
36
36
  > **🖥️ 跨平台执行规则**
37
- > - 先确认当前终端工作目录是项目根目录;若不是,先 `cd` 到项目根目录。
37
+ > - **SDD 文档根** = `*-sdd-specs` 包裹包(含 `openspec/`、`modules.yaml`),不是 Git 根或工作区根。
38
+ > - `openspec` 命令:优先 `node skywalk-sdd/openspec-shim.cjs list`(自动 cd 到包裹包),或先 `cd` 到包裹包再执行。
39
+ > - `skywalk-sdd/log.cjs` / `ontology/cli.cjs`:`--project=.` 在 Git 根/工作区根会自动解析;也可 `node skywalk-sdd/ontology/cli.cjs spec-root` 查看路径。
38
40
  > - Telemetry 命令默认使用 `--project=.`,兼容 Windows、macOS、Linux。
39
41
  > - ${SHELL_GUIDANCE}
40
42
  > - 不要省略 `--source=opsx-command` 与 `--session-id=<会话ID>`。
@@ -19,7 +19,9 @@ allowed-tools:
19
19
  你是一个 SDD 项目规则生成与维护专家。激活本技能后,你将扫描项目事实,为已部署的 Agent 生成或审查规则文件。
20
20
 
21
21
  > **🖥️ 跨平台执行规则**
22
- > - 先确认当前终端工作目录是项目根目录;若不是,先 `cd` 到项目根目录。
22
+ > - **SDD 文档根** = `*-sdd-specs` 包裹包(含 `openspec/`、`modules.yaml`),不是 Git 根或工作区根。
23
+ > - `openspec` 命令:优先 `node skywalk-sdd/openspec-shim.cjs list`(自动 cd 到包裹包),或先 `cd` 到包裹包再执行。
24
+ > - `skywalk-sdd/log.cjs` / `ontology/cli.cjs`:`--project=.` 在 Git 根/工作区根会自动解析;也可 `node skywalk-sdd/ontology/cli.cjs spec-root` 查看路径。
23
25
  > - ${SHELL_GUIDANCE}
24
26
  > - **本技能不写入 SkyWalk Telemetry**(与 `opsx-knowledge` 同属辅助技能,不走 `log.cjs start/end`)。
25
27
 
@@ -34,7 +34,9 @@ allowed-tools:
34
34
  > 阶段边界自检见 `./checklist.md`「阶段边界⛔」。
35
35
 
36
36
  > **🖥️ 跨平台执行规则**
37
- > - 先确认当前终端工作目录是项目根目录;若不是,先 `cd` 到项目根目录。
37
+ > - **SDD 文档根** = `*-sdd-specs` 包裹包(含 `openspec/`、`modules.yaml`),不是 Git 根或工作区根。
38
+ > - `openspec` 命令:优先 `node skywalk-sdd/openspec-shim.cjs list`(自动 cd 到包裹包),或先 `cd` 到包裹包再执行。
39
+ > - `skywalk-sdd/log.cjs` / `ontology/cli.cjs`:`--project=.` 在 Git 根/工作区根会自动解析;也可 `node skywalk-sdd/ontology/cli.cjs spec-root` 查看路径。
38
40
  > - Telemetry 命令默认使用 `--project=.`,兼容 Windows、macOS、Linux。
39
41
  > - ${SHELL_GUIDANCE}
40
42
  > - 不要省略 `--source=opsx-command` 与 `--session-id=<会话ID>`。
@@ -47,7 +47,9 @@ allowed-tools:
47
47
  > - ⛔ **隔离红线**:绝对禁止跨目录读取同级其他 Capability 的文档(Full 模式)
48
48
 
49
49
  > **🖥️ 跨平台执行规则**
50
- > - 先确认当前终端工作目录是项目根目录;若不是,先 `cd` 到项目根目录。
50
+ > - **SDD 文档根** = `*-sdd-specs` 包裹包(含 `openspec/`、`modules.yaml`),不是 Git 根或工作区根。
51
+ > - `openspec` 命令:优先 `node skywalk-sdd/openspec-shim.cjs list`(自动 cd 到包裹包),或先 `cd` 到包裹包再执行。
52
+ > - `skywalk-sdd/log.cjs` / `ontology/cli.cjs`:`--project=.` 在 Git 根/工作区根会自动解析;也可 `node skywalk-sdd/ontology/cli.cjs spec-root` 查看路径。
51
53
  > - Telemetry 命令默认使用 `--project=.`,兼容 Windows、macOS、Linux。
52
54
  > - ${SHELL_GUIDANCE}
53
55
  > - 不要省略 `--source=opsx-command` 与 `--session-id=<会话ID>`。
@@ -24,7 +24,9 @@ allowed-tools:
24
24
  > - 输出标准化测试报告,便于质量追踪
25
25
 
26
26
  > **🖥️ 跨平台执行规则**
27
- > - 先确认当前终端工作目录是项目根目录;若不是,先 `cd` 到项目根目录。
27
+ > - **SDD 文档根** = `*-sdd-specs` 包裹包(含 `openspec/`、`modules.yaml`),不是 Git 根或工作区根。
28
+ > - `openspec` 命令:优先 `node skywalk-sdd/openspec-shim.cjs list`(自动 cd 到包裹包),或先 `cd` 到包裹包再执行。
29
+ > - `skywalk-sdd/log.cjs` / `ontology/cli.cjs`:`--project=.` 在 Git 根/工作区根会自动解析;也可 `node skywalk-sdd/ontology/cli.cjs spec-root` 查看路径。
28
30
  > - Telemetry 命令默认使用 `--project=.`,兼容 Windows、macOS、Linux。
29
31
  > - ${SHELL_GUIDANCE}
30
32
  > - 不要省略 `--source=opsx-command` 与 `--session-id=<会话ID>`。