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.
- package/lib/hook-gate-core.js +41 -5
- package/lib/init.js +154 -31
- package/package.json +1 -1
- package/skywalk-sdd/ontology/naming-diagnose.cjs +26 -3
- package/templates/git-hooks/commit-msg-sdd-trailer.cjs +11 -1
- package/templates/hooks/claude/hooks/sdd-prompt.cjs +23 -5
- package/templates/hooks/codebuddy/hooks/sdd-prompt.cjs +22 -3
- package/templates/skills/kld-sdd/opsx-apply/SKILL.md +3 -3
- package/templates/skills/kld-sdd/opsx-apply/checklist.md +3 -3
- package/templates/skills/kld-sdd/opsx-apply/reference.md +13 -13
- package/templates/skills/kld-sdd/opsx-apply/worktree-setup.md +3 -3
- package/templates/skills/kld-sdd/opsx-archive/SKILL.md +14 -14
- package/templates/skills/kld-sdd/opsx-archive/checklist.md +2 -2
- package/templates/skills/kld-sdd/opsx-check/SKILL.md +13 -13
- package/templates/skills/kld-sdd/opsx-design/SKILL.md +4 -4
- package/templates/skills/kld-sdd/opsx-design/reference.md +2 -2
- package/templates/skills/kld-sdd/opsx-explore/SKILL.md +5 -5
- package/templates/skills/kld-sdd/opsx-propose/SKILL.md +11 -11
- package/templates/skills/kld-sdd/opsx-propose/reference.md +2 -2
- package/templates/skills/kld-sdd/opsx-rules/SKILL.md +3 -3
- package/templates/skills/kld-sdd/opsx-spec/SKILL.md +5 -5
- package/templates/skills/kld-sdd/opsx-spec/reference.md +2 -2
- package/templates/skills/kld-sdd/opsx-task/SKILL.md +4 -4
- package/templates/skills/kld-sdd/opsx-task/reference.md +2 -2
- package/templates/skills/kld-sdd/opsx-tdd-core/reference.md +3 -3
- package/templates/skills/kld-sdd/opsx-tdd-rules/rules/test-skeleton-telemetry.md +2 -2
- package/templates/skills/kld-sdd/opsx-test/SKILL.md +5 -5
package/lib/hook-gate-core.js
CHANGED
|
@@ -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
|
|
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)
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
886
|
-
|
|
887
|
-
|
|
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
|
|
908
|
-
* commit-msg-only:
|
|
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
|
-
*
|
|
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
|
|
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
|
|
1408
|
-
|
|
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
|
@@ -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 =
|
|
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 (!
|
|
99
|
+
if (!scriptPath) {
|
|
77
100
|
return {
|
|
78
101
|
status: STATUS.FIXABLE,
|
|
79
102
|
code: 'HOOK_SCRIPT_MISSING',
|
|
80
|
-
message: 'commit-msg
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|