kld-sdd 2.4.19 → 2.5.0
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 +9 -12
- package/package.json +1 -1
- package/skywalk-sdd/index.cjs +1808 -129
- package/templates/hooks/claude/hooks/sdd-apply-test-gate.cjs +175 -28
- package/templates/hooks/claude/hooks/sdd-post-tool.cjs +42 -21
- package/templates/openspec/proposal.md +0 -1
- package/templates/openspec/spec.md +2 -2
- package/templates/skills/kld-sdd/opsx-apply/SKILL.md +64 -355
- package/templates/skills/kld-sdd/opsx-apply/checklist.md +94 -0
- package/templates/skills/kld-sdd/opsx-apply/reference.md +403 -0
- package/templates/skills/kld-sdd/opsx-archive/SKILL.md +21 -5
- package/templates/skills/kld-sdd/opsx-archive/checklist.md +33 -0
- package/templates/skills/kld-sdd/opsx-check/SKILL.md +28 -4
- package/templates/skills/kld-sdd/opsx-check/checklist.md +37 -0
- package/templates/skills/kld-sdd/opsx-design/SKILL.md +46 -50
- package/templates/skills/kld-sdd/opsx-design/checklist.md +46 -0
- package/templates/skills/kld-sdd/opsx-design/reference.md +44 -0
- package/templates/skills/kld-sdd/opsx-propose/SKILL.md +51 -95
- package/templates/skills/kld-sdd/opsx-propose/checklist.md +44 -0
- package/templates/skills/kld-sdd/opsx-propose/reference.md +94 -0
- package/templates/skills/kld-sdd/opsx-spec/SKILL.md +46 -50
- package/templates/skills/kld-sdd/opsx-spec/checklist.md +46 -0
- package/templates/skills/kld-sdd/opsx-spec/reference.md +49 -0
- package/templates/skills/kld-sdd/opsx-task/SKILL.md +42 -45
- package/templates/skills/kld-sdd/opsx-task/checklist.md +46 -0
- package/templates/skills/kld-sdd/opsx-task/reference.md +40 -0
- package/templates/skills/kld-sdd/opsx-test/SKILL.md +12 -0
|
@@ -215,10 +215,17 @@ function block(decision, reason, extra) {
|
|
|
215
215
|
process.exit(2);
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
-
function recordWarning(projectRoot, applyEvent, code, message) {
|
|
218
|
+
function recordWarning(projectRoot, applyEvent, code, message, taskIds) {
|
|
219
219
|
const logPath = path.join(projectRoot, 'skywalk-sdd', 'log.cjs');
|
|
220
|
-
if (!fs.existsSync(logPath))
|
|
220
|
+
if (!fs.existsSync(logPath)) {
|
|
221
|
+
// Y8 修复:log.cjs 缺失时 stderr 提示(本 repo 无 log.cjs 属预期——脚手架部署时 deployTelemetryDataDir 复制 index.cjs→log.cjs)
|
|
222
|
+
try { console.error(`[sdd-apply-test-gate] log.cjs 不存在(${logPath}),telemetry_warning 未记录;确认 deployTelemetryDataDir 已执行`); } catch {}
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
221
225
|
try {
|
|
226
|
+
// T1.5b(v3): task_update_reuse 写入复用 task_ids,供 E4(A3) 扣分识别
|
|
227
|
+
const details = { warning: code, test_strategy: applyEvent.test_strategy };
|
|
228
|
+
if (Array.isArray(taskIds) && taskIds.length > 0) details.task_ids = taskIds;
|
|
222
229
|
const args = [
|
|
223
230
|
logPath,
|
|
224
231
|
'record',
|
|
@@ -230,7 +237,7 @@ function recordWarning(projectRoot, applyEvent, code, message) {
|
|
|
230
237
|
'--source=claude-hook',
|
|
231
238
|
'--result=partial',
|
|
232
239
|
`--summary=${message}`,
|
|
233
|
-
`--details-json=${JSON.stringify(
|
|
240
|
+
`--details-json=${JSON.stringify(details)}`,
|
|
234
241
|
];
|
|
235
242
|
if (applyEvent.capability) args.push(`--capability=${applyEvent.capability}`);
|
|
236
243
|
if (applyEvent.session_id) args.push(`--session-id=${applyEvent.session_id}`);
|
|
@@ -280,36 +287,176 @@ function shouldGate(projectRoot, applyEvent) {
|
|
|
280
287
|
return { strategy, changeName: applyEvent.change, capability: applyEvent.capability };
|
|
281
288
|
}
|
|
282
289
|
|
|
283
|
-
// ──
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
290
|
+
// ── T1.1: task_update 复用检测(不阻断,仅记 warning) ──
|
|
291
|
+
/**
|
|
292
|
+
* 检测同一 change 的 implementation task_update 是否复用同一次测试结果。
|
|
293
|
+
* 判定:同 session_id + 同 test_results 三元组(passed+failed+duration_ms) 的 group 内 >1 条(不再要求时间戳相邻)。
|
|
294
|
+
* 测试确实跑过,只是 task_update 记录不实 → 记 telemetry_warning(task_update_reuse),不阻断。
|
|
295
|
+
* 返回 { reused, groups: [{ duration_ms, fingerprint, session_id, task_ids }] }
|
|
296
|
+
*/
|
|
297
|
+
function detectReusedTestResults(events) {
|
|
298
|
+
const taskUpdates = (events || []).filter((e) =>
|
|
299
|
+
e && e.type === 'task_update' &&
|
|
300
|
+
e.details && e.details.test_results &&
|
|
301
|
+
isRealTestDetails(e.details.test_results)
|
|
302
|
+
);
|
|
303
|
+
const bySession = new Map();
|
|
304
|
+
for (const e of taskUpdates) {
|
|
305
|
+
const sid = e.session_id || '_no_session_';
|
|
306
|
+
if (!bySession.has(sid)) bySession.set(sid, []);
|
|
307
|
+
bySession.get(sid).push(e);
|
|
308
|
+
}
|
|
309
|
+
const groups = [];
|
|
310
|
+
let reused = false;
|
|
311
|
+
for (const [, group] of bySession) {
|
|
312
|
+
const byFingerprint = new Map();
|
|
313
|
+
for (const e of group) {
|
|
314
|
+
const tr = e.details.test_results;
|
|
315
|
+
const passed = Number(tr.passed);
|
|
316
|
+
const failed = Number(tr.failed);
|
|
317
|
+
const dur = Number(tr.duration_ms);
|
|
318
|
+
if (!Number.isFinite(dur)) continue;
|
|
319
|
+
const fp = `${passed}|${failed}|${dur}`;
|
|
320
|
+
if (!byFingerprint.has(fp)) byFingerprint.set(fp, []);
|
|
321
|
+
byFingerprint.get(fp).push(e);
|
|
322
|
+
}
|
|
323
|
+
for (const [fp, evs] of byFingerprint) {
|
|
324
|
+
if (evs.length < 2) continue;
|
|
325
|
+
const sorted = evs.slice().sort((a, b) => new Date(a.timestamp || 0) - new Date(b.timestamp || 0));
|
|
326
|
+
// T1.5(v3): 去掉 <5s 相邻限制 — 同 session + 同三元组即复用,无论时间间隔
|
|
327
|
+
// todo-cli 真实场景:实现任务与验证任务间隔 15-17s,<5s 阈值漏检 3/4 组
|
|
328
|
+
reused = true;
|
|
329
|
+
const first = sorted[0];
|
|
330
|
+
groups.push({
|
|
331
|
+
duration_ms: Number(first.details.test_results.duration_ms),
|
|
332
|
+
fingerprint: fp,
|
|
333
|
+
session_id: first.session_id || null,
|
|
334
|
+
task_ids: sorted.map((e) => e.task_id || e.event_id).filter(Boolean),
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return { reused, groups };
|
|
294
339
|
}
|
|
295
340
|
|
|
296
|
-
|
|
297
|
-
|
|
341
|
+
/** T1.6: 检测本阶段是否有失败/门禁拦截但无 process_note 过程记录。
|
|
342
|
+
* Y4 修复:when 参数过滤时间窗口(仅检测 sinceTimestamp 后的事件),避免 change 级误报。
|
|
343
|
+
* 返回 { gap, hasFailure, hasGateBlock, hasProcessNote } */
|
|
344
|
+
function detectProcessNoteGap(events, sinceTimestamp) {
|
|
345
|
+
const evs = events || [];
|
|
346
|
+
const filtered = sinceTimestamp ? evs.filter(e => new Date(e.timestamp || 0).getTime() >= new Date(sinceTimestamp).getTime()) : evs;
|
|
347
|
+
const stageEnds = filtered.filter((e) => e.type === 'stage_end');
|
|
348
|
+
const hasFailure = stageEnds.some((e) => e.result === 'failure' || e.result === 'partial');
|
|
349
|
+
const hasGateBlock = filtered.some((e) =>
|
|
350
|
+
e.type === 'telemetry_warning' && e.details && e.details.warning === 'apply_test_missing'
|
|
351
|
+
);
|
|
352
|
+
const hasProcessNote = filtered.some((e) => e.type === 'process_note');
|
|
353
|
+
const gap = (hasFailure || hasGateBlock) && !hasProcessNote;
|
|
354
|
+
return { gap, hasFailure, hasGateBlock, hasProcessNote };
|
|
355
|
+
}
|
|
298
356
|
|
|
299
|
-
|
|
300
|
-
|
|
357
|
+
/** B2(v3): 检测 tdd 策略下 apply 阶段是否缺失"红灯"(失败的测试结果)。
|
|
358
|
+
* TDD 节奏要求测试骨架先 RED(failed>0)再实现转 GREEN;若 apply 阶段有真实测试信号
|
|
359
|
+
* 但全部 failed===0,说明骨架可能未经 RED 验证即标记完成 → 记 telemetry_warning(tdd_red_missing),不阻断。
|
|
360
|
+
* 不复用 isRealTestDetails(其要求 command 非空,会漏掉无 command 的骨架 test_results)。
|
|
361
|
+
* 返回 { missing, hasRed, signalCount } */
|
|
362
|
+
function detectMissingTddRed(events, sinceTimestamp) {
|
|
363
|
+
const evs = events || [];
|
|
364
|
+
const filtered = sinceTimestamp ? evs.filter(e => new Date(e.timestamp || 0).getTime() >= new Date(sinceTimestamp).getTime()) : evs;
|
|
365
|
+
let signalCount = 0;
|
|
366
|
+
let hasRed = false;
|
|
367
|
+
for (const e of filtered) {
|
|
368
|
+
const tr = e && e.details && (e.details.test_results || e.details.test);
|
|
369
|
+
if (!tr || typeof tr !== 'object') continue;
|
|
370
|
+
const passed = Number(tr.passed);
|
|
371
|
+
const failed = Number(tr.failed);
|
|
372
|
+
const dur = Number(tr.duration_ms);
|
|
373
|
+
const isReal = (Number.isFinite(passed) && passed > 0)
|
|
374
|
+
|| (Number.isFinite(failed) && failed > 0)
|
|
375
|
+
|| (Number.isFinite(dur) && dur > 0);
|
|
376
|
+
if (!isReal) continue;
|
|
377
|
+
signalCount++;
|
|
378
|
+
if (Number.isFinite(failed) && failed > 0) hasRed = true;
|
|
379
|
+
}
|
|
380
|
+
if (signalCount === 0) return { missing: false, hasRed: false, signalCount: 0, reason: 'no-real-test-signals' };
|
|
381
|
+
return { missing: !hasRed, hasRed, signalCount };
|
|
301
382
|
}
|
|
302
383
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
384
|
+
/** 是否已存在某 code 的 telemetry_warning(去重,避免每次 hook 触发都重复记录) */
|
|
385
|
+
function hasExistingWarning(events, code) {
|
|
386
|
+
return (events || []).some((e) =>
|
|
387
|
+
e.type === 'telemetry_warning' && e.details && e.details.warning === code
|
|
388
|
+
);
|
|
306
389
|
}
|
|
307
390
|
|
|
308
|
-
|
|
309
|
-
|
|
391
|
+
// ── 主逻辑(仅直接执行时运行;被 require 时跳过,便于单元测试) ──
|
|
392
|
+
if (require.main === module) {
|
|
393
|
+
const input = parseInput(readStdin());
|
|
394
|
+
const projectRoot = getProjectRoot(input);
|
|
395
|
+
const toolName = String(input.tool_name || input.toolName || '');
|
|
396
|
+
const toolInput = input.tool_input || input.toolInput || {};
|
|
397
|
+
const command = String(toolInput.command || '');
|
|
398
|
+
|
|
399
|
+
const activeApply = findActiveApplyStage(projectRoot);
|
|
400
|
+
if (!activeApply) {
|
|
401
|
+
process.exit(0);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const bashGate = toolName === 'Bash' && isApplyCompletionBash(command);
|
|
405
|
+
const stopGate = !toolName || toolName === 'Stop';
|
|
406
|
+
|
|
407
|
+
if (!bashGate && !stopGate) {
|
|
408
|
+
process.exit(0);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// 非阻断警告检测(T1.1 task_update 复用 / T1.6 process_note 缺失):放行但记录 warning
|
|
412
|
+
try {
|
|
413
|
+
const warningEvents = loadChangeEvents(projectRoot, activeApply.change);
|
|
414
|
+
const warningCtx = { ...activeApply, test_strategy: readTestStrategy(projectRoot, activeApply.change) };
|
|
415
|
+
const reuse = detectReusedTestResults(warningEvents);
|
|
416
|
+
if (reuse.reused && !hasExistingWarning(warningEvents, 'task_update_reuse')) {
|
|
417
|
+
const taskCount = reuse.groups.reduce((s, g) => s + g.task_ids.length, 0);
|
|
418
|
+
const reusedTaskIds = reuse.groups.flatMap((g) => g.task_ids).filter(Boolean);
|
|
419
|
+
recordWarning(projectRoot, warningCtx, 'task_update_reuse',
|
|
420
|
+
`检测到 ${reuse.groups.length} 组 task_update 复用同一次测试结果(共 ${taskCount} 条),测试确实跑过但记录不实`,
|
|
421
|
+
reusedTaskIds);
|
|
422
|
+
}
|
|
423
|
+
const noteGap = detectProcessNoteGap(warningEvents, activeApply.timestamp);
|
|
424
|
+
if (noteGap.gap && !hasExistingWarning(warningEvents, 'process_note_missing')) {
|
|
425
|
+
recordWarning(projectRoot, warningCtx, 'process_note_missing',
|
|
426
|
+
'本阶段存在失败/门禁拦截但无 process_note 过程记录,建议补记');
|
|
427
|
+
}
|
|
428
|
+
// B2(v3): tdd 策略下 apply 阶段无红灯(失败 test_result)→ 软警告,不阻断
|
|
429
|
+
if (warningCtx.test_strategy === 'tdd') {
|
|
430
|
+
const redGap = detectMissingTddRed(warningEvents, activeApply.timestamp);
|
|
431
|
+
if (redGap.missing && !hasExistingWarning(warningEvents, 'tdd_red_missing')) {
|
|
432
|
+
recordWarning(projectRoot, warningCtx, 'tdd_red_missing',
|
|
433
|
+
'tdd 策略下 apply 阶段未检测到失败的测试结果(红灯),测试骨架可能未经 RED 验证即标记完成');
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
} catch {
|
|
437
|
+
// 警告检测失败不阻断主流程
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const gate = shouldGate(projectRoot, activeApply);
|
|
441
|
+
if (!gate) {
|
|
442
|
+
process.exit(0);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const reason = buildReason(gate.strategy, gate.changeName, gate.capability);
|
|
446
|
+
recordWarning(projectRoot, { ...activeApply, test_strategy: gate.strategy }, 'apply_test_missing', 'Apply test gate blocked: no unit test evidence');
|
|
447
|
+
|
|
448
|
+
block('block', reason, {
|
|
449
|
+
test_strategy: gate.strategy,
|
|
450
|
+
change: gate.changeName,
|
|
451
|
+
capability: gate.capability,
|
|
452
|
+
});
|
|
453
|
+
}
|
|
310
454
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
455
|
+
module.exports = {
|
|
456
|
+
detectReusedTestResults,
|
|
457
|
+
detectProcessNoteGap,
|
|
458
|
+
detectMissingTddRed,
|
|
459
|
+
hasExistingWarning,
|
|
460
|
+
isRealTestDetails,
|
|
461
|
+
requiresUnitTests,
|
|
462
|
+
};
|
|
@@ -70,15 +70,23 @@ function latestActiveStage(projectRoot) {
|
|
|
70
70
|
.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())[0] || null;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
// inferResult:解析 tool_response 判定构建/测试结果
|
|
74
|
+
// 返回 { result: 'success'|'failure'|'partial', exit_code_known: boolean }
|
|
75
|
+
// v2.5 契约变更:返回类型从 string 变为对象(含 exit_code_known 标注);旧调用方 `inferResult(input) === 'success'` 需改为 `.result === 'success'`
|
|
73
76
|
function inferResult(input) {
|
|
74
77
|
const response = input.tool_response || input.toolResponse || {};
|
|
75
|
-
if (typeof response.exit_code === 'number') return response.exit_code === 0 ? 'success' : 'failure';
|
|
76
|
-
if (typeof response.exitCode === 'number') return response.exitCode === 0 ? 'success' : 'failure';
|
|
77
|
-
if (typeof response.success === 'boolean') return response.success ? 'success' : 'failure';
|
|
78
|
-
|
|
78
|
+
if (typeof response.exit_code === 'number') return { result: response.exit_code === 0 ? 'success' : 'failure', exit_code_known: true };
|
|
79
|
+
if (typeof response.exitCode === 'number') return { result: response.exitCode === 0 ? 'success' : 'failure', exit_code_known: true };
|
|
80
|
+
if (typeof response.success === 'boolean') return { result: response.success ? 'success' : 'failure', exit_code_known: true };
|
|
81
|
+
// fallback:从 stdout/stderr 识别构建失败信号;无法判定时 partial 且 exit_code_known=false
|
|
82
|
+
const out = String(response.stdout || response.stderr || '');
|
|
83
|
+
if (/BUILD FAILED|error TS\d+|ERR_/i.test(out)) return { result: 'failure', exit_code_known: false };
|
|
84
|
+
return { result: 'partial', exit_code_known: false };
|
|
79
85
|
}
|
|
80
86
|
|
|
81
87
|
function inferRecord(command) {
|
|
88
|
+
// 排除 telemetry 自身的 record 命令(防止 log.cjs/log.js record 被误判为 build/test 结果)
|
|
89
|
+
if (/\b(log\.cjs|log\.js)\s+record\b/.test(command)) return null;
|
|
82
90
|
if (/\b(npm|pnpm|yarn)\s+(test|run\s+test)\b|\bpytest\b|\bmvn\b.*\btest\b|\bgo\s+test\b|\bcargo\s+test\b/i.test(command)) {
|
|
83
91
|
return {
|
|
84
92
|
type: 'test_result',
|
|
@@ -96,23 +104,29 @@ function inferRecord(command) {
|
|
|
96
104
|
return null;
|
|
97
105
|
}
|
|
98
106
|
|
|
99
|
-
function recordTelemetry(projectRoot, activeStage, record, result) {
|
|
107
|
+
function recordTelemetry(projectRoot, activeStage, record, result, exitCodeKnown, input, runner) {
|
|
100
108
|
const logPath = path.join(projectRoot, 'skywalk-sdd', 'log.cjs');
|
|
101
109
|
const legacyLogPath = path.join(projectRoot, 'skywalk-sdd', 'log.js');
|
|
102
110
|
const logCli = fs.existsSync(logPath) ? logPath : legacyLogPath;
|
|
103
111
|
if (!fs.existsSync(logCli)) return;
|
|
104
112
|
|
|
113
|
+
const toolInput = (input && (input.tool_input || input.toolInput)) || {};
|
|
114
|
+
const toolResponse = (input && (input.tool_response || input.toolResponse)) || {};
|
|
105
115
|
const details = { [record.detailsKey]: { ...record.details } };
|
|
106
116
|
if (record.type === 'build_result') {
|
|
107
117
|
details.build_results.success = result === 'success';
|
|
108
118
|
details.build_results.error_count = result === 'success' ? 0 : null;
|
|
119
|
+
details.build_results.exit_code_known = Boolean(exitCodeKnown);
|
|
120
|
+
const durationMs = typeof toolResponse.duration_ms === 'number' ? toolResponse.duration_ms : null;
|
|
121
|
+
details.build_results.duration_ms = durationMs;
|
|
122
|
+
details.build_results.duration_known = durationMs !== null;
|
|
109
123
|
}
|
|
110
124
|
|
|
111
125
|
const args = [
|
|
112
126
|
logCli,
|
|
113
127
|
'record',
|
|
114
128
|
`--type=${record.type}`,
|
|
115
|
-
`--command=${activeStage.command || activeStage.stage || 'unknown'}`,
|
|
129
|
+
`--command=${toolInput.command || activeStage.command || activeStage.stage || 'unknown'}`,
|
|
116
130
|
`--project=${projectRoot}`,
|
|
117
131
|
`--change=${activeStage.change || 'general'}`,
|
|
118
132
|
`--agent=${activeStage.agent_type || 'claude-code'}`,
|
|
@@ -124,23 +138,30 @@ function recordTelemetry(projectRoot, activeStage, record, result) {
|
|
|
124
138
|
if (activeStage.capability) args.push(`--capability=${activeStage.capability}`);
|
|
125
139
|
if (activeStage.task_id) args.push(`--task-id=${activeStage.task_id}`);
|
|
126
140
|
if (activeStage.session_id) args.push(`--session-id=${activeStage.session_id}`);
|
|
127
|
-
execFileSync('node',
|
|
141
|
+
const run = runner || ((a) => execFileSync('node', a, { cwd: projectRoot, stdio: 'ignore' }));
|
|
142
|
+
run(args);
|
|
128
143
|
}
|
|
129
144
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
|
|
145
|
+
// 仅在直接执行时跑主流程;require 时不读 stdin(便于单元测试)
|
|
146
|
+
if (require.main === module) {
|
|
147
|
+
const input = parseInput(readStdin());
|
|
148
|
+
const toolInput = input.tool_input || input.toolInput || {};
|
|
149
|
+
const command = String(toolInput.command || input.command || '');
|
|
150
|
+
const record = inferRecord(command);
|
|
151
|
+
if (!record) process.exit(0);
|
|
135
152
|
|
|
136
|
-
const projectRoot = findProjectRoot(input);
|
|
137
|
-
const activeStage = latestActiveStage(projectRoot);
|
|
138
|
-
if (!activeStage || !activeStage.change || activeStage.change === 'general') {
|
|
139
|
-
|
|
140
|
-
}
|
|
153
|
+
const projectRoot = findProjectRoot(input);
|
|
154
|
+
const activeStage = latestActiveStage(projectRoot);
|
|
155
|
+
if (!activeStage || !activeStage.change || activeStage.change === 'general') {
|
|
156
|
+
process.exit(0);
|
|
157
|
+
}
|
|
141
158
|
|
|
142
|
-
try {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
159
|
+
try {
|
|
160
|
+
const inferred = inferResult(input);
|
|
161
|
+
recordTelemetry(projectRoot, activeStage, record, inferred.result, inferred.exit_code_known, input);
|
|
162
|
+
} catch {
|
|
163
|
+
process.exit(0);
|
|
164
|
+
}
|
|
146
165
|
}
|
|
166
|
+
|
|
167
|
+
module.exports = { inferResult, inferRecord, recordTelemetry, findProjectRoot, latestActiveStage };
|