kld-sdd 2.4.17 → 2.4.19
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 +62 -32
- package/package.json +2 -1
- package/skywalk-sdd/apply-worktree-finish.cjs +551 -0
- package/skywalk-sdd/index.cjs +6 -0
- package/templates/hooks/claude/hooks/sdd-apply-test-gate.cjs +315 -0
- package/templates/hooks/claude/hooks/sdd-skill-apply-gate.cjs +3 -2
- package/templates/hooks/claude/settings.json +8 -0
- package/templates/skills/kld-sdd/opsx-apply/SKILL.md +219 -11
- package/templates/skills/kld-sdd/opsx-apply/worktree-setup.md +64 -101
- package/templates/skills/kld-sdd/opsx-archive/SKILL.md +10 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* SDD Apply Test Gate Hook
|
|
4
|
+
*
|
|
5
|
+
* 当 proposal 选择单元测试策略(test-strategy: tdd | impl-first)时,
|
|
6
|
+
* 在 apply 收尾前校验是否已有真实测试执行证据;缺失则阻断并提示模型执行测试。
|
|
7
|
+
*
|
|
8
|
+
* 触发点:
|
|
9
|
+
* - PreToolUse(Bash): apply-worktree-finish(非 --record-base)、log.cjs end(apply 阶段)
|
|
10
|
+
* - Stop: 存在活跃 apply 阶段且即将结束会话
|
|
11
|
+
*
|
|
12
|
+
* 退出码:0 放行 | 2 阻断(stdout JSON decision:block)
|
|
13
|
+
*/
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const { execFileSync } = require('child_process');
|
|
19
|
+
|
|
20
|
+
function readStdin() {
|
|
21
|
+
try {
|
|
22
|
+
return fs.readFileSync(0, 'utf8');
|
|
23
|
+
} catch {
|
|
24
|
+
return '';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function parseInput(raw) {
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(raw || '{}');
|
|
31
|
+
} catch {
|
|
32
|
+
return {};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function hasTelemetryCli(dir) {
|
|
37
|
+
return fs.existsSync(path.join(dir, 'skywalk-sdd', 'log.cjs')) ||
|
|
38
|
+
fs.existsSync(path.join(dir, 'skywalk-sdd', 'log.js'));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function findProjectRoot(startDir) {
|
|
42
|
+
if (!startDir) return '';
|
|
43
|
+
let current = path.resolve(startDir);
|
|
44
|
+
for (let i = 0; i < 25; i++) {
|
|
45
|
+
if (hasTelemetryCli(current)) return current;
|
|
46
|
+
const parent = path.dirname(current);
|
|
47
|
+
if (parent === current) return '';
|
|
48
|
+
current = parent;
|
|
49
|
+
}
|
|
50
|
+
return '';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getProjectRoot(input) {
|
|
54
|
+
const toolInput = input.tool_input || input.toolInput || {};
|
|
55
|
+
const candidates = [
|
|
56
|
+
toolInput.cwd,
|
|
57
|
+
input.cwd,
|
|
58
|
+
input.project_root,
|
|
59
|
+
process.env.CLAUDE_PROJECT_DIR,
|
|
60
|
+
process.env.PWD,
|
|
61
|
+
process.cwd(),
|
|
62
|
+
].filter(Boolean);
|
|
63
|
+
for (const dir of candidates) {
|
|
64
|
+
const root = findProjectRoot(dir);
|
|
65
|
+
if (root) return root;
|
|
66
|
+
}
|
|
67
|
+
return process.cwd();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function safeChangeName(name) {
|
|
71
|
+
if (!name) return '';
|
|
72
|
+
return String(name)
|
|
73
|
+
.toLowerCase()
|
|
74
|
+
.replace(/[\s_]+/g, '-')
|
|
75
|
+
.replace(/[^a-z0-9一-鿿\-]/g, '-')
|
|
76
|
+
.replace(/-+/g, '-')
|
|
77
|
+
.replace(/^-|-$/g, '');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function findActiveApplyStage(projectRoot) {
|
|
81
|
+
const stateDir = path.join(projectRoot, 'skywalk-sdd', 'state');
|
|
82
|
+
if (!fs.existsSync(stateDir)) return null;
|
|
83
|
+
|
|
84
|
+
let latest = null;
|
|
85
|
+
for (const file of fs.readdirSync(stateDir).filter((f) => f.endsWith('.json'))) {
|
|
86
|
+
try {
|
|
87
|
+
const data = JSON.parse(fs.readFileSync(path.join(stateDir, file), 'utf8'));
|
|
88
|
+
const event = data.event || null;
|
|
89
|
+
if (event && event.command === 'apply') {
|
|
90
|
+
if (!latest || new Date(event.timestamp) > new Date(latest.timestamp)) {
|
|
91
|
+
latest = event;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
// skip
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return latest;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function findProposalPath(projectRoot, changeName) {
|
|
102
|
+
const candidates = [
|
|
103
|
+
path.join(projectRoot, 'openspec', 'changes', changeName, 'proposal.md'),
|
|
104
|
+
path.join(projectRoot, 'changes', changeName, 'proposal.md'),
|
|
105
|
+
];
|
|
106
|
+
for (const p of candidates) {
|
|
107
|
+
if (fs.existsSync(p)) return p;
|
|
108
|
+
}
|
|
109
|
+
const safe = safeChangeName(changeName);
|
|
110
|
+
if (safe && safe !== changeName) {
|
|
111
|
+
return findProposalPath(projectRoot, safe);
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function readTestStrategy(projectRoot, changeName) {
|
|
117
|
+
const proposalPath = findProposalPath(projectRoot, changeName);
|
|
118
|
+
if (!proposalPath) return null;
|
|
119
|
+
try {
|
|
120
|
+
const content = fs.readFileSync(proposalPath, 'utf8');
|
|
121
|
+
const fm = content.match(/^---\s*[\r\n]+([\s\S]*?)[\r\n]+---/);
|
|
122
|
+
if (!fm) return null;
|
|
123
|
+
const m = fm[1].match(/^\s*test-strategy:\s*["']?([a-z-]+)["']?\s*$/im);
|
|
124
|
+
return m ? m[1].trim() : null;
|
|
125
|
+
} catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function requiresUnitTests(strategy) {
|
|
131
|
+
return strategy === 'tdd' || strategy === 'impl-first';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function isRealTestDetails(testResults) {
|
|
135
|
+
if (!testResults || typeof testResults !== 'object') return false;
|
|
136
|
+
const command = String(testResults.command || '').trim();
|
|
137
|
+
if (!command) return false;
|
|
138
|
+
const passed = Number(testResults.passed);
|
|
139
|
+
const failed = Number(testResults.failed);
|
|
140
|
+
const duration = Number(testResults.duration_ms);
|
|
141
|
+
if (Number.isFinite(passed) && passed > 0) return true;
|
|
142
|
+
if (Number.isFinite(failed) && failed > 0) return true;
|
|
143
|
+
if (Number.isFinite(duration) && duration > 0) return true;
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function loadChangeEvents(projectRoot, changeName) {
|
|
148
|
+
const safe = safeChangeName(changeName);
|
|
149
|
+
const dir = path.join(projectRoot, 'skywalk-sdd', 'events', safe);
|
|
150
|
+
if (!fs.existsSync(dir)) return [];
|
|
151
|
+
|
|
152
|
+
const events = [];
|
|
153
|
+
for (const file of fs.readdirSync(dir).filter((f) => f.endsWith('.jsonl'))) {
|
|
154
|
+
try {
|
|
155
|
+
const lines = fs.readFileSync(path.join(dir, file), 'utf8').split('\n').filter(Boolean);
|
|
156
|
+
for (const line of lines) {
|
|
157
|
+
try {
|
|
158
|
+
events.push(JSON.parse(line));
|
|
159
|
+
} catch {
|
|
160
|
+
// skip
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
} catch {
|
|
164
|
+
// skip
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return events;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function hasRealTestExecution(projectRoot, changeName, capability, sinceTimestamp) {
|
|
171
|
+
const since = sinceTimestamp ? new Date(sinceTimestamp).getTime() : 0;
|
|
172
|
+
const events = loadChangeEvents(projectRoot, changeName);
|
|
173
|
+
|
|
174
|
+
for (const event of events) {
|
|
175
|
+
const ts = new Date(event.timestamp || 0).getTime();
|
|
176
|
+
if (since && ts < since) continue;
|
|
177
|
+
|
|
178
|
+
if (event.type === 'stage_end' && event.command === 'test' &&
|
|
179
|
+
(event.result === 'success' || event.result === 'failure')) {
|
|
180
|
+
return { kind: 'opsx-test', event };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (event.type === 'test_result') {
|
|
184
|
+
const tr = event.details && event.details.test_results;
|
|
185
|
+
if (isRealTestDetails(tr)) {
|
|
186
|
+
return { kind: 'test_result', event };
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (event.type === 'task_update' && (event.command === 'apply' || !event.command)) {
|
|
191
|
+
const tr = event.details && event.details.test_results;
|
|
192
|
+
if (isRealTestDetails(tr)) {
|
|
193
|
+
if (!capability || !event.capability || event.capability === capability) {
|
|
194
|
+
return { kind: 'task_update', event };
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function isApplyCompletionBash(command) {
|
|
203
|
+
const cmd = String(command || '');
|
|
204
|
+
if (/apply-worktree-finish\.cjs/.test(cmd) && !/--record-base/.test(cmd)) {
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
if (/(?:skywalk-sdd\/)?log\.cjs\s+end\b/.test(cmd)) {
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function block(decision, reason, extra) {
|
|
214
|
+
console.log(JSON.stringify({ decision, reason, ...extra }));
|
|
215
|
+
process.exit(2);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function recordWarning(projectRoot, applyEvent, code, message) {
|
|
219
|
+
const logPath = path.join(projectRoot, 'skywalk-sdd', 'log.cjs');
|
|
220
|
+
if (!fs.existsSync(logPath)) return;
|
|
221
|
+
try {
|
|
222
|
+
const args = [
|
|
223
|
+
logPath,
|
|
224
|
+
'record',
|
|
225
|
+
'--type=telemetry_warning',
|
|
226
|
+
'--command=apply',
|
|
227
|
+
`--project=${projectRoot}`,
|
|
228
|
+
`--change=${applyEvent.change || 'general'}`,
|
|
229
|
+
'--agent=claude-code',
|
|
230
|
+
'--source=claude-hook',
|
|
231
|
+
'--result=partial',
|
|
232
|
+
`--summary=${message}`,
|
|
233
|
+
`--details-json=${JSON.stringify({ warning: code, test_strategy: applyEvent.test_strategy })}`,
|
|
234
|
+
];
|
|
235
|
+
if (applyEvent.capability) args.push(`--capability=${applyEvent.capability}`);
|
|
236
|
+
if (applyEvent.session_id) args.push(`--session-id=${applyEvent.session_id}`);
|
|
237
|
+
execFileSync('node', args, { cwd: projectRoot, stdio: 'ignore' });
|
|
238
|
+
} catch {
|
|
239
|
+
// ignore
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function buildReason(strategy, changeName, capability) {
|
|
244
|
+
const cap = capability ? ` / ${capability}` : '';
|
|
245
|
+
const strict = strategy === 'tdd';
|
|
246
|
+
const lines = [
|
|
247
|
+
`[SDD Apply Test Gate] 变更 ${changeName}${cap} 的 test-strategy=${strategy},但尚未检测到真实单元测试执行记录。`,
|
|
248
|
+
'',
|
|
249
|
+
'请先在本项目根目录(或 apply worktree)执行单元测试,例如:',
|
|
250
|
+
' npm test / pnpm test / pytest / go test ./... / cargo test',
|
|
251
|
+
'',
|
|
252
|
+
'执行后应产生以下任一 telemetry 证据:',
|
|
253
|
+
' - skywalk-sdd 事件 test_result(含实际 command 与 passed/failed/duration)',
|
|
254
|
+
' - task_update 中 test_results 非空占位',
|
|
255
|
+
' - 或运行 /opsx-test 完成 test 阶段',
|
|
256
|
+
'',
|
|
257
|
+
strict
|
|
258
|
+
? 'tdd 策略:测试未执行前不得结束 apply 或执行 apply-worktree-finish。'
|
|
259
|
+
: 'impl-first 策略:实现后须补跑测试并确认通过,再结束 apply。',
|
|
260
|
+
'',
|
|
261
|
+
'完成后可重试当前操作。',
|
|
262
|
+
];
|
|
263
|
+
return lines.join('\n');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function shouldGate(projectRoot, applyEvent) {
|
|
267
|
+
const strategy = readTestStrategy(projectRoot, applyEvent.change);
|
|
268
|
+
if (!requiresUnitTests(strategy)) {
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
const evidence = hasRealTestExecution(
|
|
272
|
+
projectRoot,
|
|
273
|
+
applyEvent.change,
|
|
274
|
+
applyEvent.capability,
|
|
275
|
+
applyEvent.timestamp
|
|
276
|
+
);
|
|
277
|
+
if (evidence) {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
return { strategy, changeName: applyEvent.change, capability: applyEvent.capability };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ── 主逻辑 ─────────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
const input = parseInput(readStdin());
|
|
286
|
+
const projectRoot = getProjectRoot(input);
|
|
287
|
+
const toolName = String(input.tool_name || input.toolName || '');
|
|
288
|
+
const toolInput = input.tool_input || input.toolInput || {};
|
|
289
|
+
const command = String(toolInput.command || '');
|
|
290
|
+
|
|
291
|
+
const activeApply = findActiveApplyStage(projectRoot);
|
|
292
|
+
if (!activeApply) {
|
|
293
|
+
process.exit(0);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const bashGate = toolName === 'Bash' && isApplyCompletionBash(command);
|
|
297
|
+
const stopGate = !toolName || toolName === 'Stop';
|
|
298
|
+
|
|
299
|
+
if (!bashGate && !stopGate) {
|
|
300
|
+
process.exit(0);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const gate = shouldGate(projectRoot, activeApply);
|
|
304
|
+
if (!gate) {
|
|
305
|
+
process.exit(0);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const reason = buildReason(gate.strategy, gate.changeName, gate.capability);
|
|
309
|
+
recordWarning(projectRoot, { ...activeApply, test_strategy: gate.strategy }, 'apply_test_missing', 'Apply test gate blocked: no unit test evidence');
|
|
310
|
+
|
|
311
|
+
block('block', reason, {
|
|
312
|
+
test_strategy: gate.strategy,
|
|
313
|
+
change: gate.changeName,
|
|
314
|
+
capability: gate.capability,
|
|
315
|
+
});
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* PreToolUse(Skill) 门禁 —— 在 opsx-apply Skill 被调用的瞬间阻断,不再等到 Write/Edit 阶段。
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* L1: sdd-skill-apply-gate (PreToolUse/Skill) ← 本文件
|
|
6
|
+
* 门禁体系:
|
|
7
|
+
* L1: sdd-skill-apply-gate (PreToolUse/Skill) ← 本文件 — check 前置
|
|
8
8
|
* L2: sdd-apply-gate (PreToolUse/Write|Edit)
|
|
9
9
|
* L3: SKILL.md Step 1.2 (Skill 自检)
|
|
10
|
+
* L4: sdd-apply-test-gate (PreToolUse/Bash + Stop) — apply 后单元测试真实执行
|
|
10
11
|
*
|
|
11
12
|
* 判定逻辑:
|
|
12
13
|
* - 非 opsx-apply 调用 → 放行
|
|
@@ -26,6 +26,10 @@
|
|
|
26
26
|
{
|
|
27
27
|
"type": "command",
|
|
28
28
|
"command": "node \"${CLAUDE_PROJECT_DIR}/.claude/hooks/sdd-pre-tool.cjs\""
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"type": "command",
|
|
32
|
+
"command": "node \"${CLAUDE_PROJECT_DIR}/.claude/hooks/sdd-apply-test-gate.cjs\""
|
|
29
33
|
}
|
|
30
34
|
]
|
|
31
35
|
},
|
|
@@ -53,6 +57,10 @@
|
|
|
53
57
|
"Stop": [
|
|
54
58
|
{
|
|
55
59
|
"hooks": [
|
|
60
|
+
{
|
|
61
|
+
"type": "command",
|
|
62
|
+
"command": "node \"${CLAUDE_PROJECT_DIR}/.claude/hooks/sdd-apply-test-gate.cjs\""
|
|
63
|
+
},
|
|
56
64
|
{
|
|
57
65
|
"type": "command",
|
|
58
66
|
"command": "node \"${CLAUDE_PROJECT_DIR}/.claude/hooks/sdd-stop.cjs\""
|
|
@@ -146,25 +146,170 @@ openspec list --json
|
|
|
146
146
|
>
|
|
147
147
|
> 是否要现在执行 check?"
|
|
148
148
|
|
|
149
|
-
> **🔗
|
|
149
|
+
> **🔗 门禁体系**:
|
|
150
|
+
> - Check:`sdd-skill-apply-gate` + `sdd-apply-gate` + 本技能 §1.2
|
|
151
|
+
> - 单元测试(`test-strategy: tdd | impl-first`):`sdd-apply-test-gate` 在 `log.cjs end` / `apply-worktree-finish` / Stop 时校验是否**真实执行**过测试
|
|
150
152
|
|
|
151
|
-
### 1.5
|
|
153
|
+
### 1.5 【本地并行】Worktree 与工作区(建议性策略)
|
|
152
154
|
|
|
153
|
-
>
|
|
154
|
-
>
|
|
155
|
+
> **目标(本质)**:在**本地仓库**加快 SDD Apply——让**解耦**的 spec/capability 可以各自实现、互不踩目录,做完再按依赖顺序合并回你**当时所在的分支**(集成分支,如 `hotfix-1`)。
|
|
156
|
+
>
|
|
157
|
+
> **不是**:保护 `master`、不是「主分支只放文档」、不是远程流水线分支策略。一切基于**本地 Git + 本地 worktree**。
|
|
158
|
+
|
|
159
|
+
#### 两层并行(不要混为一谈)
|
|
160
|
+
|
|
161
|
+
| 层级 | 手段 | 适用 |
|
|
162
|
+
|------|------|------|
|
|
163
|
+
| **Capability 级**(跨 spec) | 每个解耦的 capability **单独**跑一次 `/opsx-apply` + **独立** worktree/apply 分支 | proposal 中能力**可并行**、无文件/接口/数据强依赖 |
|
|
164
|
+
| **Task 级**(cap 内) | **同一** worktree 内,DAG 同层子代理并行 | tasks.md 同层任务不改相同文件 |
|
|
165
|
+
|
|
166
|
+
- 本技能单次仍只实施**一个** capability;「多 spec 并行」= 多个 apply 会话 + 多个 worktree,不是一次加载多个 capability 文档。
|
|
167
|
+
- §5 子代理并行 **不** 再建 worktree;Capability 级并行在 §1.5 决定「本次是否建 worktree」。
|
|
168
|
+
|
|
169
|
+
#### 何时建议建 worktree / 按 capability 切分支?(模型判断,非强制)
|
|
170
|
+
|
|
171
|
+
**倾向创建**(full 模式常见:`.worktrees/apply-<change>-<capability>` + `kld-sdd/<change>/<capability>`):
|
|
172
|
+
|
|
173
|
+
- `proposal.md` §3 中该 capability 与并行中的其他能力**无**「修改同一模块 / 共享表 / 必须先合入」等描述
|
|
174
|
+
- `design.md` 显示文件路径、包、表与并行中的其他 cap **基本不重叠**
|
|
175
|
+
- 用户需要在**同一集成分支**上并行推进多个 cap,且机器资源允许(多份 `node_modules`)
|
|
176
|
+
|
|
177
|
+
**倾向不建 / 串行 apply**(仍在集成分支上改,或只开一个 worktree):
|
|
178
|
+
|
|
179
|
+
- proposal 写明 capability **依赖**(B 依赖 A 已合入)
|
|
180
|
+
- 多 cap 改**同一文件/模块/配置**(merge 冲突几乎必然)
|
|
181
|
+
- 数据库迁移、公共类型、单例注册等**顺序敏感**
|
|
182
|
+
- 已有另一个 capability 的 worktree **尚未 finish merge** 到集成分支,且当前 cap 需要基于**最新**集成结果开发
|
|
183
|
+
- 沙箱禁止 `git worktree add` → 回退当前目录 + 功能分支,并告知用户
|
|
184
|
+
|
|
185
|
+
**full 模式「一 capability 一分支」**:是**命名与目录约定 + 建议**,须先通过下方 **§1.5 Step 0.1 校验** 再命名/建 worktree。simple 模式通常**一 change 一 worktree**。
|
|
186
|
+
|
|
187
|
+
#### Step 0.1 【必做】分支/隔离校验(早于 record-base 与 `git worktree add`)
|
|
188
|
+
|
|
189
|
+
> 在建议分支名 `kld-sdd/<change>/<capability>` 或创建 worktree **之前**,用 `proposal.md` + 各 capability **spec 依赖信息** 做交叉校验。未通过则**不**按 full 并行策略拆分支,改为串行或本次不建 worktree。
|
|
190
|
+
|
|
191
|
+
**Step A — 读变更级能力清单(允许读 proposal,不读其它 cap 的 design/tasks)**
|
|
192
|
+
|
|
193
|
+
从 `changes/<change-name>/proposal.md` 提取:
|
|
194
|
+
|
|
195
|
+
| 字段 | 用途 |
|
|
196
|
+
|------|------|
|
|
197
|
+
| frontmatter `mode` | `full` → 倾向 `apply-<change>-<cap>`;`simple` → 倾向 `apply-<change>` |
|
|
198
|
+
| §3.1 / §3.2 能力列表 | 本 change 下全部 capability 名 |
|
|
199
|
+
| §4.2 依赖关系图 | 能力间先后 / 上下游 |
|
|
200
|
+
| §5.3 前置依赖 checkbox | 未满足则当前 cap 不宜并行 |
|
|
201
|
+
|
|
202
|
+
**Step B — 读当前 capability 的 spec 依赖(§2 正式加载前仅此文件)**
|
|
203
|
+
|
|
204
|
+
读取**当前** capability 的 `spec.md`(路径按 mode):
|
|
205
|
+
|
|
206
|
+
- **full**:`changes/<change>/specs/<capability>/spec.md` 或 `openspec/changes/.../specs/<capability>/spec.md`(以项目实际为准)
|
|
207
|
+
- **simple**:`changes/<change>/spec.md`
|
|
208
|
+
|
|
209
|
+
只重点读 spec 中:**能力边界 / 涉及模块 / §4 内部与外部依赖**(是否写明依赖其它 capability、共享表、必须先发布的接口)。
|
|
210
|
+
|
|
211
|
+
**Step C — 跨 capability 轻量交叉(隔离例外,仅本节)**
|
|
212
|
+
|
|
213
|
+
对 §3 中**其它** capability,**只**读取其 `spec.md` 的:
|
|
214
|
+
|
|
215
|
+
- 标题与能力简述
|
|
216
|
+
- **§4 依赖**(是否依赖 `<当前 capability>` 或其它 cap)
|
|
217
|
+
- **涉及模块 / 数据表 / 公共配置**(若有)
|
|
218
|
+
|
|
219
|
+
⛔ 禁止为校验而加载其它 cap 的 `design.md`、`tasks.md`(完整实施上下文仍遵守 §2 隔离红线)。
|
|
220
|
+
|
|
221
|
+
**Step D — 判定矩阵(输出给用户)**
|
|
222
|
+
|
|
223
|
+
| 校验项 | 不通过时的处理 |
|
|
224
|
+
|--------|----------------|
|
|
225
|
+
| proposal §4.2 / §5.3 写明当前 cap **依赖** 未完成的其它 cap | **串行**:先 finish 上游,再 apply 当前;不并行拆分支 |
|
|
226
|
+
| 其它 cap 的 spec 写明 **依赖当前 cap** 且当前 cap 未 finish | 当前可并行实施,但提醒下游须等本次 finish |
|
|
227
|
+
| 多 cap spec 出现**相同模块路径 / 同表 / 同配置文件** | **不并行** worktree;串行或合并为一个 apply 范围 |
|
|
228
|
+
| 当前 cap spec §4 要求接口/表已由其它 cap 提供 | 若其它 cap 未 merge 到集成分支 → **不建** worktree,先完成上游 |
|
|
229
|
+
| `git branch --list 'kld-sdd/<change>/*'` 已存在同名分支 | 复用既有 worktree/分支,或询问用户是否清理后重建 |
|
|
230
|
+
| 沙箱 / 环境无法 `worktree add` | 不建 worktree;集成分支上直接开发 |
|
|
231
|
+
|
|
232
|
+
**Step E — 分支与目录命名(校验通过后)**
|
|
233
|
+
|
|
234
|
+
| mode | worktree 目录(建议) | apply 分支(建议) |
|
|
235
|
+
|------|----------------------|-------------------|
|
|
236
|
+
| full + 可隔离 | `.worktrees/apply-<change>-<capability>` | `kld-sdd/<change>/<capability>` |
|
|
237
|
+
| simple 或 change 内单实现体 | `.worktrees/apply-<change>` | `kld-sdd/<change>/<capability>`(cap 可与 change 同名) |
|
|
238
|
+
| 校验不通过但仍需隔离 | `.worktrees/apply-<change>-<capability>` 或单 worktree | 仍用 `kld-sdd/<change>/<capability>`,但**不得**与其它 cap 并行 |
|
|
239
|
+
|
|
240
|
+
**报告模板**(创建 worktree 前必须输出):
|
|
241
|
+
|
|
242
|
+
```
|
|
243
|
+
📋 Apply 隔离校验 — <change>/<capability>
|
|
244
|
+
- mode: full | simple
|
|
245
|
+
- 本 change 能力域: [cap-a, cap-b, …]
|
|
246
|
+
- 与当前 cap 冲突/依赖: [无 | cap-a 必须先合入 | 与 cap-b 共享模块 X]
|
|
247
|
+
- 并行建议: [可独立 worktree | 串行等待 cap-a | 不建 worktree,原地 apply]
|
|
248
|
+
- 分支(建议): kld-sdd/<change>/<capability>
|
|
249
|
+
- 集成分支(将 record-base): <当前 git 分支>
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
#### 多 Capability 并行时的合并顺序(本地)
|
|
253
|
+
|
|
254
|
+
1. 各 capability 在各自 worktree 内完成 + `§6.1 finish` 前,先根据 `proposal.md` 排出 **capability 依赖 DAG**。
|
|
255
|
+
2. **按 DAG 顺序**依次 `finish` merge 到**同一** `integration_base`(先 A 合入,再 B;B 的 worktree 若基于旧尖端,merge 前应在 B 的 worktree 内 `merge/rebase integration_base` 或由用户选择重建 worktree)。
|
|
256
|
+
3. 有依赖的 cap **不要**与上游 cap 同时 finish;无依赖的可并行实施,但 **merge 仍建议逐个** 以降低冲突。
|
|
257
|
+
|
|
258
|
+
向用户简要说明判断结果:
|
|
259
|
+
> "📌 并行建议:cap-A、cap-C 可并行 worktree;cap-B 依赖 A,待 A finish 后再 apply。"
|
|
260
|
+
|
|
261
|
+
#### 分支 / Worktree 创建时机(勿与子代理混淆)
|
|
262
|
+
|
|
263
|
+
| 时机 | 做什么 | 谁执行 |
|
|
264
|
+
|------|--------|--------|
|
|
265
|
+
| **§1.5 Step 0.25~1(仅一次)** | 记录集成分支 → 创建 worktree + 新建 `kld-sdd/<change>/<capability>` | **主会话** |
|
|
266
|
+
| §2~§4 | 读文档、解析 DAG | 主会话 |
|
|
267
|
+
| **§5 子代理** | 在**已有** worktree 内实现任务 | **子代理**(不建 worktree、不建分支) |
|
|
268
|
+
| **§6.1** | merge 回集成分支 → remove worktree → 删 apply 分支 | **主会话** + `apply-worktree-finish.cjs` |
|
|
269
|
+
|
|
270
|
+
- 集成分支 = Apply **开始时**主仓库 `git branch --show-current`(例:用户在 `hotfix-1` 上开始,则 merge 回 `hotfix-1`)。
|
|
271
|
+
- `git worktree add -b` 在主仓库处于集成分支时执行,apply 分支从该分支尖端分出。
|
|
272
|
+
- **子代理禁止** `EnterWorktree` / `git worktree add` / `checkout -b`;同层并行共享**同一** worktree 与 apply 分支。
|
|
155
273
|
|
|
156
274
|
**设置流程**(详见 `./worktree-setup.md`):
|
|
157
275
|
|
|
158
276
|
**Step 0: 检测当前状态**
|
|
159
|
-
- 若已是 Git Worktree →
|
|
277
|
+
- 若已是 Git Worktree → 跳过创建(集成分支应已在 state 文件中)
|
|
160
278
|
- 若非 Git 项目(`vcs_mode=no-git`)→ 跳过
|
|
161
279
|
|
|
280
|
+
**Step 0.1: 分支/隔离校验** — 见上文「Step 0.1 【必做】」,**通过后再执行** 0.25 / 0.5 / Step 1
|
|
281
|
+
|
|
282
|
+
**Step 0.25: 记录集成分支(Git 项目必做,创建 worktree 之前)**
|
|
283
|
+
|
|
284
|
+
在主仓库根目录、**尚未** `cd` 进 `.worktrees/` 时执行:
|
|
285
|
+
|
|
286
|
+
```bash
|
|
287
|
+
node skywalk-sdd/apply-worktree-finish.cjs --record-base \
|
|
288
|
+
--change=<变更名称> \
|
|
289
|
+
--capability=<capability-name>
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
- 将当前具名分支写入 `skywalk-sdd/state/apply-<change>-<capability>.json` 的 `integration_base`
|
|
293
|
+
- 若为 detached HEAD,先 `git checkout` 到具名分支再记录
|
|
294
|
+
- simple 模式可省略 `--capability`(默认与 change 相同)
|
|
295
|
+
|
|
296
|
+
**Step 0.5: 主仓库冲突预检(Git 项目必做)**
|
|
297
|
+
- 在主仓库根目录执行 `git status`,确认**无**与本次 Apply 将修改路径冲突的**未跟踪**实现文件(如 `src/`、`package.json` 等)
|
|
298
|
+
- 若存在,先移走、提交或删除;否则收尾 `merge` 会报 `untracked working tree files would be overwritten`
|
|
299
|
+
- 收尾脚本默认启用 `--check-untracked` 预检并给出明确报错
|
|
300
|
+
|
|
162
301
|
**Step 1: 创建隔离工作区**
|
|
302
|
+
|
|
303
|
+
> 确认主仓库仍检出在 **Step 0.25 记录的集成分支**(或与其一致的尖端),再创建 worktree。
|
|
304
|
+
|
|
163
305
|
- **首选**:使用 `EnterWorktree` 工具(Claude Code 原生)
|
|
164
306
|
```
|
|
165
307
|
EnterWorktree(name="apply-<change-name>-<capability-name>")
|
|
166
308
|
```
|
|
167
309
|
- **回退**:仅当原生工具不可用时,使用 `git worktree add`
|
|
310
|
+
- **full 模式**目录:`.worktrees/apply-<change-name>-<capability-name>`
|
|
311
|
+
- **simple 模式**目录:`.worktrees/apply-<change-name>`(无 capability 后缀)
|
|
312
|
+
- **分支命名**:仅当 Step 0.1 校验通过后,使用报告中的 `kld-sdd/<change-name>/<capability-name>`(勿对强依赖 cap 强行并行拆分支)
|
|
168
313
|
```bash
|
|
169
314
|
git worktree add .worktrees/apply-<change-name>-<capability-name> \
|
|
170
315
|
-b kld-sdd/<change-name>/<capability-name>
|
|
@@ -178,6 +323,7 @@ openspec list --json
|
|
|
178
323
|
|
|
179
324
|
**报告**:
|
|
180
325
|
> "✅ Worktree 就绪:`<path>`
|
|
326
|
+
> 集成分支:`<integration_base>`(收尾将 merge 回此分支)
|
|
181
327
|
> 基线测试通过(N 个测试,0 失败)
|
|
182
328
|
> 准备实施 `<change-name>/<capability-name>`"
|
|
183
329
|
|
|
@@ -243,6 +389,7 @@ b. **显示当前层级任务**
|
|
|
243
389
|
c. **🤖 派发子代理实现代码**
|
|
244
390
|
|
|
245
391
|
> **使用 Agent 工具并行派发同层任务,每个子代理负责一个任务,上下文隔离、专注高效。**
|
|
392
|
+
> **⛔ 子代理阶段不创建 worktree/分支**——隔离环境已在 §1.5 就绪;子代理仅在 worktree 目录内实现 TASK。
|
|
246
393
|
|
|
247
394
|
**派发准备(每个子代理):**
|
|
248
395
|
1. 从 tasks.md 中提取该任务的完整描述
|
|
@@ -333,11 +480,66 @@ node skywalk-sdd/log.cjs record --type=ai_adoption_review --command=apply --proj
|
|
|
333
480
|
> 建议执行 `/kld-review --local` 对本次变更的代码进行评审。
|
|
334
481
|
> 该技能可在公司内部 SkillHub 下载安装。
|
|
335
482
|
>
|
|
336
|
-
> **🧹 Worktree
|
|
337
|
-
>
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
483
|
+
> **🧹 Worktree 收尾**:若本次使用了 worktree,见 **§6.1**;若判定未建 worktree,跳过 §6.1。
|
|
484
|
+
> **其他 capability**:若并行 apply 中仍有未 finish 的 worktree,提示按 proposal 依赖顺序继续 `finish`。
|
|
485
|
+
|
|
486
|
+
### 6.0 【条件必做】单元测试真实执行(`test-strategy` 非 `none`)
|
|
487
|
+
|
|
488
|
+
当 `proposal.md` 的 `test-strategy` 为 **`tdd`** 或 **`impl-first`** 时:
|
|
489
|
+
|
|
490
|
+
1. **在结束 apply 或执行 §6.1 收尾之前**,必须在项目根或 worktree 内**真实运行**单元测试命令(`npm test` / `pytest` / `go test` 等)。
|
|
491
|
+
2. 须留下可核验的 telemetry 证据(任选其一):
|
|
492
|
+
- `node skywalk-sdd/log.cjs record --type=test_result ...`(`test_results.command` 非空,且 `passed`/`failed`/`duration_ms` 有实际值)
|
|
493
|
+
- `task_update` 的 `details-json` 中 `test_results` 含真实执行数据
|
|
494
|
+
- 或单独运行 `/opsx-test` 并完成 `command=test` 的 `stage_end`
|
|
495
|
+
3. **Claude Code**:`sdd-apply-test-gate.cjs` 会在 `log.cjs end`、`apply-worktree-finish`、会话 Stop 时自动校验;无证据则**阻断**并提示补跑测试。
|
|
496
|
+
|
|
497
|
+
| test-strategy | 行为 |
|
|
498
|
+
|---------------|------|
|
|
499
|
+
| `tdd` | 无测试证据不得结束 apply / 不得 finish worktree |
|
|
500
|
+
| `impl-first` | 同上,实现后必须补跑并记录 |
|
|
501
|
+
| `none` | 跳过本节与 test gate |
|
|
502
|
+
|
|
503
|
+
### 6.1 【条件必做】Worktree 收尾(仅当 §1.5 已创建 worktree)
|
|
504
|
+
|
|
505
|
+
> **⛔ 本次 Apply 若创建了 worktree**:DAG 全部完成且 **§6.0 测试门禁(若适用)** 通过后,必须在**主仓库根目录**执行收尾脚本(禁止在 `.worktrees/...` 内执行)。
|
|
506
|
+
> 若 §1.5 判定未建 worktree(串行、强依赖、沙箱回退),跳过本节。
|
|
507
|
+
|
|
508
|
+
**前置条件**:
|
|
509
|
+
- worktree 内 `git status` 干净(实现代码已全部 commit)
|
|
510
|
+
- 主仓库无与 merge 冲突的未跟踪文件(见 §1.5 Step 0.5)
|
|
511
|
+
- `test-strategy` 为 `tdd`/`impl-first` 时,§6.0 测试证据已存在
|
|
512
|
+
|
|
513
|
+
**执行**(主仓库根目录):
|
|
514
|
+
```bash
|
|
515
|
+
node skywalk-sdd/apply-worktree-finish.cjs \
|
|
516
|
+
--change=<变更名称> \
|
|
517
|
+
--capability=<capability-name>
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
- **默认 merge 目标**:§1.5 `--record-base` 写入的 `integration_base`(**不是**默认 master)
|
|
521
|
+
- 仅当 state 缺失或需覆盖时传 `--target=<集成分支>`
|
|
522
|
+
- **simple 模式**:`--capability` 可省略;目录为 `.worktrees/apply-<change>`
|
|
523
|
+
- **full 模式**:目录为 `.worktrees/apply-<change>-<capability>`
|
|
524
|
+
|
|
525
|
+
**脚本行为**:`checkout 集成分支` → `merge --no-ff` apply 分支 → `worktree remove` → 删临时目录 → `branch -d` apply 分支
|
|
526
|
+
|
|
527
|
+
**常用 flags**:`--dry-run` 预演;`--skip-merge` 已手动 merge;`--keep-branch` 保留 apply 分支;`--no-check-untracked` 跳过未跟踪预检
|
|
528
|
+
|
|
529
|
+
**收尾 Telemetry(推荐)**:
|
|
530
|
+
```bash
|
|
531
|
+
node skywalk-sdd/log.cjs record --type=worktree_finish \
|
|
532
|
+
--command=apply --project=. --change=<变更名称> --capability=<capability-name> \
|
|
533
|
+
--agent=<Agent类型> --source=opsx-command --session-id=<会话ID> \
|
|
534
|
+
--result=success --summary="merge+remove completed" \
|
|
535
|
+
--details-json='{"integration_base":"<branch>","merge_commit":"<sha>","worktree_removed":true}'
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
**回退**(仅当脚本不可用时,详见 `./worktree-setup.md`):
|
|
539
|
+
- `EnterWorktree`:`ExitWorktree(action="remove", discard_changes=false)`
|
|
540
|
+
- `git worktree add`:`git worktree remove` + `git branch -D`
|
|
541
|
+
|
|
542
|
+
> **与 Git 只读策略的关系**:Apply **实施过程**禁止 Agent 随意 commit;**收尾**由本脚本执行 merge/remove,属于流程必做步骤,不视为「随意提交业务代码」。
|
|
341
543
|
|
|
342
544
|
---
|
|
343
545
|
|
|
@@ -348,9 +550,15 @@ node skywalk-sdd/log.cjs record --type=ai_adoption_review --command=apply --proj
|
|
|
348
550
|
- **⛔ 隔离红线**:绝对禁止加载同级其他 Capability 的文档
|
|
349
551
|
- **⛔ DAG 依赖拦截**:执行任务前必须检查依赖,前置未完成必须拦截
|
|
350
552
|
- **⛔ 编译检查门禁**:每完成一个任务后,**必须运行编译检查**,编译失败禁止标记已完成
|
|
351
|
-
- **⛔ 测试执行门禁**:根据 `test-strategy` 决定测试门禁行为(tdd=强制, impl-first
|
|
553
|
+
- **⛔ 测试执行门禁**:根据 `test-strategy` 决定测试门禁行为(tdd=强制, impl-first=强制补跑, none=跳过);须真实执行并留 telemetry,`sdd-apply-test-gate` 会校验非占位数据
|
|
352
554
|
- **⛔ 必须实时更新任务状态**:每完成一个任务,**立即**修改 tasks.md 中的 `- [ ]` 为 `- [x]`,**同时修改** `**状态**: [ ] 未完成` 为 `**状态**: [x] 已完成`,两种格式不可遗漏
|
|
353
555
|
- **Git 只读策略**:禁止为了度量自动初始化 Git、创建分支或提交 commit;非 Git 项目使用 `vcs_mode=no-git` 继续执行
|
|
556
|
+
- **⛔ Step 0.1 隔离校验必做**:建 worktree / 建议分支名前,必须完成 proposal + 跨 cap spec 依赖校验并输出报告;未通过不得按 full 并行策略拆 `kld-sdd/<change>/<cap>`
|
|
557
|
+
- **Worktree 为加速手段,非必选项**:校验通过且解耦方可多 worktree;有依赖或共享修改面则串行
|
|
558
|
+
- **本地集成分支**:使用 worktree 时,`--record-base` 记录用户**当前本地分支**为 merge 目标,禁止默认写死 `master`
|
|
559
|
+
- **⛔ 已建 worktree 则收尾必做**:`apply-worktree-finish.cjs` merge 回 `integration_base`;多 cap 并行时按依赖顺序逐个 finish
|
|
560
|
+
- **⛔ 子代理不建分支**:worktree/apply 分支仅在 §1.5(主会话)创建一次,§5 子代理不得再建
|
|
561
|
+
- **Capability 级并行**:多个 `/opsx-apply` 会话 = 多个 worktree;禁止为未解耦的 cap 仅因 full 模式而拆分支
|
|
354
562
|
- **显示进度反馈**:每完成一个任务,显示「✅ [TASK-ID] 已完成 [N/M]」
|
|
355
563
|
- **保持任务聚焦**:每次只处理一个任务
|
|
356
564
|
- **遇到问题时暂停**:任务描述模糊、编译失败、测试失败或发现设计问题时暂停询问
|