sillyspec 3.28.10 → 3.28.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/.claude/skills/sillyspec-execute/SKILL.md +223 -222
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.claude/skills/sillyspec-quick/SKILL.md +8 -10
- package/.claude/skills/sillyspec-verify/SKILL.md +91 -87
- package/.husky/pre-commit +4 -0
- package/.husky/pre-push +3 -0
- package/AGENTS.md +1 -1
- package/package.json +1 -1
- package/src/change-risk-profile.js +76 -6
- package/src/commit-guard.js +119 -0
- package/src/config-schema.js +23 -0
- package/src/design-facts.js +46 -9
- package/src/diagnostic-codes.js +95 -0
- package/src/docs-check.js +14 -10
- package/src/docs-debt.js +7 -2
- package/src/docs-gate.js +42 -15
- package/src/doctor-diagnostics.js +141 -1
- package/src/endpoint-extractor.js +50 -2
- package/src/foreign-declared.js +25 -16
- package/src/hooks/worktree-guard.js +75 -2
- package/src/index.js +129 -1
- package/src/machine-interface.js +63 -0
- package/src/probe7-anchor-check.js +20 -2
- package/src/progress/stage-machine.js +34 -5
- package/src/progress.js +1 -1
- package/src/quicklog.js +231 -1
- package/src/run/complete-handlers.js +172 -1
- package/src/run/complete.js +73 -3
- package/src/run/gate-snapshot.js +262 -0
- package/src/run/gates.js +41 -7
- package/src/run/prompt.js +79 -1
- package/src/run/shared.js +10 -0
- package/src/run/verify-quality-scan.js +151 -5
- package/src/stage-contract-spec.js +14 -0
- package/src/stage-contract.js +738 -8
- package/src/stage-review-checklist.js +16 -2
- package/src/stage-review.js +137 -2
- package/src/stages/archive.js +9 -5
- package/src/stages/brainstorm.js +7 -1
- package/src/stages/execute.js +62 -8
- package/src/stages/plan-postcheck.js +42 -5
- package/src/stages/plan.js +8 -1
- package/src/stages/quick.js +5 -5
- package/src/stages/verify.js +8 -3
- package/src/sync.js +69 -0
- package/src/task-review.js +9 -3
- package/src/verify-facts-schema.js +95 -10
- package/src/verify-postcheck.js +346 -28
- package/src/verify-probes.js +1221 -46
- package/src/worktree-apply.js +175 -3
- package/src/write-audit.js +55 -0
- package/.claude/full.json +0 -1
|
@@ -20,10 +20,15 @@
|
|
|
20
20
|
* 4. evaluateConclusionDraft(前置三·结论草稿不进判定链):机械事实全绿(实测通过 + lint
|
|
21
21
|
* 非 failed + 探针 1/3/5/6 干净 + 风险门非 integration/deployment-critical)才给 PASS
|
|
22
22
|
* 草稿;gate 对结论槽的独立复核逻辑不动——草稿只省打字成本,槽行仍由 agent 复核可改写。
|
|
23
|
+
* 5. smoke 亲跑执行段(2026-09-17-api-coverage-smoke task-01 / FR-01):commands.smoke
|
|
24
|
+
* 显式配置才执行(300s 超时帽/快照内超时回退主仓复跑一次),stdout+stderr tee 实录落
|
|
25
|
+
* .runtime/verify-logs/smoke-<change>.log;失败/超时只记 additive smokeResult 失败态
|
|
26
|
+
* **不 throw**(封顶信号归 --done PASS 封顶消费,非崩溃——与 test failed 的 throw 语义
|
|
27
|
+
* 刻意区分)。指纹已含 commands 段原文——配置 smoke 前后指纹自动变化,代码未变不重跑。
|
|
23
28
|
*/
|
|
24
29
|
import { createHash } from 'node:crypto'
|
|
25
|
-
import { execSync } from 'node:child_process'
|
|
26
|
-
import { existsSync, readFileSync, mkdirSync } from 'node:fs'
|
|
30
|
+
import { execSync, spawnSync } from 'node:child_process'
|
|
31
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
27
32
|
import { join, dirname } from 'node:path'
|
|
28
33
|
import { writeAtomicSync } from '../fs-atomic.js'
|
|
29
34
|
import { gitQuiet } from '../git-helper.js'
|
|
@@ -88,8 +93,11 @@ export function computeQualityScanFingerprint({ cwd, specBase }) {
|
|
|
88
93
|
/**
|
|
89
94
|
* 实测结果 + 指纹落盘(noAI 动作调用;--done 复用读回)。fail-soft:落盘失败只 warn,
|
|
90
95
|
* --done 将照旧亲测(复用是优化不是正确性依赖)。
|
|
96
|
+
* smokeResult(2026-09-17-api-coverage-smoke task-01)为 additive 段:RECORD_SCHEMA_VERSION
|
|
97
|
+
* 保持 1——存量记录无 smoke 段照常读回(smokeResult 兜底 null);未配置 smoke 时段形态为
|
|
98
|
+
* configured=false(记录仍写,供 --done PASS 封顶判 not-configured)。
|
|
91
99
|
*/
|
|
92
|
-
export function storeQualityScan({ specBase, cwd, changeName, testResult, lintResult }) {
|
|
100
|
+
export function storeQualityScan({ specBase, cwd, changeName, testResult, lintResult, smokeResult }) {
|
|
93
101
|
const path = qualityScanRecordPath(specBase, changeName)
|
|
94
102
|
if (!path) return null
|
|
95
103
|
const fingerprint = computeQualityScanFingerprint({ cwd, specBase })
|
|
@@ -103,6 +111,7 @@ export function storeQualityScan({ specBase, cwd, changeName, testResult, lintRe
|
|
|
103
111
|
ranAt: new Date().toISOString(),
|
|
104
112
|
testResult,
|
|
105
113
|
lintResult: lintResult || null,
|
|
114
|
+
smokeResult: smokeResult || null,
|
|
106
115
|
}, null, 2) + '\n')
|
|
107
116
|
return { path, fingerprint }
|
|
108
117
|
} catch (e) {
|
|
@@ -114,6 +123,7 @@ export function storeQualityScan({ specBase, cwd, changeName, testResult, lintRe
|
|
|
114
123
|
/**
|
|
115
124
|
* 读回复用记录:schema/source 校验 + 指纹当前匹配 + testResult 有效(passed/skipped;
|
|
116
125
|
* failed 不复用——失败态指纹下步骤本就未完成,防御性兜底)。任一不满足 → null。
|
|
126
|
+
* 复用条件只判 testResult(smoke 段随整条记录复用,additive 读回 smokeResult)。
|
|
117
127
|
*/
|
|
118
128
|
export function loadReusableQualityScan({ specBase, cwd, changeName }) {
|
|
119
129
|
const path = qualityScanRecordPath(specBase, changeName)
|
|
@@ -125,7 +135,7 @@ export function loadReusableQualityScan({ specBase, cwd, changeName }) {
|
|
|
125
135
|
if (rec.testResult.status === 'failed') return null
|
|
126
136
|
const fp = computeQualityScanFingerprint({ cwd, specBase })
|
|
127
137
|
if (!fp || fp !== rec.fingerprint) return null
|
|
128
|
-
return { testResult: rec.testResult, lintResult: rec.lintResult || null, ranAt: rec.ranAt || null, fingerprint: fp }
|
|
138
|
+
return { testResult: rec.testResult, lintResult: rec.lintResult || null, smokeResult: rec.smokeResult || null, ranAt: rec.ranAt || null, fingerprint: fp }
|
|
129
139
|
} catch {
|
|
130
140
|
return null
|
|
131
141
|
}
|
|
@@ -245,6 +255,131 @@ export function renderCoverageExistenceReport(cov) {
|
|
|
245
255
|
}
|
|
246
256
|
|
|
247
257
|
|
|
258
|
+
// ── smoke 亲跑执行段(2026-09-17-api-coverage-smoke task-01,FR-01 / D-001@v1 / D-010@v1)──
|
|
259
|
+
// 300s 超时帽与 gate-snapshot.js runGateSnapshotCommands 同款(300_000 先例)。
|
|
260
|
+
const SMOKE_TIMEOUT_MS = 300_000
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* 从 local.yaml 文本提取 commands.smoke。
|
|
264
|
+
* 与 extractTestCommand 同风格同容错(带引号/不带引号;'unavailable' 视为未配置——
|
|
265
|
+
* 未配置/unavailable 口径对齐 coverage 先例:执行段休眠,记录 configured=false)。
|
|
266
|
+
* 模块内私有(runSmokeCheck 消费)。
|
|
267
|
+
*/
|
|
268
|
+
function extractSmokeCommand(yamlText) {
|
|
269
|
+
if (!yamlText) return null
|
|
270
|
+
const doubleQuoted = yamlText.match(/^\s*smoke:\s*"([^"]+)"\s*(?:#.*)?$/m)
|
|
271
|
+
const singleQuoted = yamlText.match(/^\s*smoke:\s*'([^']+)'\s*(?:#.*)?$/m)
|
|
272
|
+
const quoted = doubleQuoted || singleQuoted
|
|
273
|
+
if (quoted && quoted[1]) {
|
|
274
|
+
return quoted[1].toLowerCase() === 'unavailable' ? null : quoted[1].trim()
|
|
275
|
+
}
|
|
276
|
+
// bare 值不排除引号字符(extractTestCommand 坑 verify-bare-quote-miss 同款修正):
|
|
277
|
+
// 全引号形态已被上方 quoted 分支先行消费,此处引号只可能是嵌在值中部的。
|
|
278
|
+
const bare = yamlText.match(/^\s*smoke:[ \t]*([^\n#]+?)[ \t]*(?:#.*)?$/m)
|
|
279
|
+
if (bare && bare[1]) {
|
|
280
|
+
const cmd = bare[1].trim()
|
|
281
|
+
return cmd.toLowerCase() === 'unavailable' ? null : cmd
|
|
282
|
+
}
|
|
283
|
+
return null
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** spawnSync 结果的超时判定(gate-snapshot.js runGateSnapshotCommands 同款口径)。 */
|
|
287
|
+
function isSyncTimeout(r) {
|
|
288
|
+
return Boolean(r && r.error && (r.error.code === 'ETIMEDOUT' || /ETIMEDOUT/i.test(String(r.error.message || ''))))
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* noAI smoke 亲跑:local.yaml 显式配置 commands.smoke 才执行(detect 不自动写,缺省零行为)。
|
|
293
|
+
* 命令脚本自理服务生命周期(后台起服+轮询就绪+finally 杀),CLI 只施加 300s 超时帽并 tee
|
|
294
|
+
* 实录 stdout+stderr 落 .runtime/verify-logs/smoke-<change>.log。失败语义与 test/lint 刻意
|
|
295
|
+
* 区分:非零退出/超时只记 smokeResult 失败态**不 throw**(封顶信号非崩溃——PASS 封顶消费
|
|
296
|
+
* 归 --done 第五条件 / task-03),本步推进语义不动。快照内超时 → 主仓 cwd 复跑一次
|
|
297
|
+
* (lint 快照超时回退同款策略:junction I/O 病态慢时保实测不假败)。
|
|
298
|
+
* 返回纯 JSON 可序列化(落进质量扫描记录 additive smokeResult 段)。
|
|
299
|
+
*/
|
|
300
|
+
function runSmokeCheck({ gateCwd, mainCwd, specBase, changeName, inSnapshot }) {
|
|
301
|
+
const ranAt = new Date().toISOString()
|
|
302
|
+
let yamlText = null
|
|
303
|
+
try { yamlText = readFileSync(join(specBase, 'local.yaml'), 'utf8') } catch { /* 无 local.yaml → 未配置 */ }
|
|
304
|
+
const command = extractSmokeCommand(yamlText)
|
|
305
|
+
if (!command) {
|
|
306
|
+
return {
|
|
307
|
+
configured: false, status: 'skipped', command: null,
|
|
308
|
+
exitCode: null, durationMs: null, logPath: null, ranAt,
|
|
309
|
+
timeout: false, fallbackMainRepo: false, source: 'cli-noai-smoke',
|
|
310
|
+
reason: yamlText ? '未配置 commands.smoke(或标记 unavailable)——执行段跳过(配置后 verify 自动亲跑)' : `local.yaml 不可读(${join(specBase, 'local.yaml')})`,
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
const startedAt = Date.now()
|
|
314
|
+
let r = null
|
|
315
|
+
let spawnError = null
|
|
316
|
+
try {
|
|
317
|
+
r = spawnSync(command, { shell: true, cwd: gateCwd, timeout: SMOKE_TIMEOUT_MS, encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 })
|
|
318
|
+
} catch (e) { spawnError = e }
|
|
319
|
+
let timedOut = isSyncTimeout(r)
|
|
320
|
+
let fallbackMainRepo = false
|
|
321
|
+
let ranCwd = gateCwd
|
|
322
|
+
// 快照内超时回退主仓复跑一次(lint 快照超时回退先例同款策略,主仓复跑保实测不假败)
|
|
323
|
+
if (inSnapshot && timedOut) {
|
|
324
|
+
fallbackMainRepo = true
|
|
325
|
+
console.warn('⚠️ 快照 smoke 超时(node_modules junction I/O 慢)→ 主仓复跑 smoke(并行噪声可能混入——失败先做归属鉴定)')
|
|
326
|
+
try {
|
|
327
|
+
r = spawnSync(command, { shell: true, cwd: mainCwd, timeout: SMOKE_TIMEOUT_MS, encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 })
|
|
328
|
+
spawnError = null
|
|
329
|
+
ranCwd = mainCwd
|
|
330
|
+
timedOut = isSyncTimeout(r)
|
|
331
|
+
} catch (e) { spawnError = e; timedOut = false }
|
|
332
|
+
}
|
|
333
|
+
const durationMs = Date.now() - startedAt
|
|
334
|
+
const exitCode = r && typeof r.status === 'number' ? r.status : null
|
|
335
|
+
const failed = timedOut || Boolean(spawnError) || (r ? r.status !== 0 : true)
|
|
336
|
+
const reason = timedOut
|
|
337
|
+
? `超时(>300s 帽${fallbackMainRepo ? ',主仓复跑仍超时' : ''})`
|
|
338
|
+
: spawnError
|
|
339
|
+
? `spawn 异常:${spawnError.message || spawnError}`
|
|
340
|
+
: (exitCode !== 0 ? `smoke 命令退出码 ${exitCode}` : null)
|
|
341
|
+
// log 实录落盘(fail-soft:写失败只 warn 置 logPath=null,不影响判定与步骤推进)
|
|
342
|
+
let logPath = null
|
|
343
|
+
if (changeName) {
|
|
344
|
+
try {
|
|
345
|
+
const logDir = join(specBase, '.runtime', 'verify-logs')
|
|
346
|
+
mkdirSync(logDir, { recursive: true })
|
|
347
|
+
logPath = join(logDir, `smoke-${changeName}.log`)
|
|
348
|
+
const head = [
|
|
349
|
+
'# smoke 实录(source: cli-noai-smoke)',
|
|
350
|
+
`# command: ${command}`,
|
|
351
|
+
`# cwd: ${ranCwd}${fallbackMainRepo ? '(快照超时回退主仓)' : ''}`,
|
|
352
|
+
`# ranAt: ${ranAt} durationMs: ${durationMs} timeout: ${timedOut}`,
|
|
353
|
+
`# exitCode: ${exitCode === null ? '(无——spawn 异常/被信号杀)' : exitCode}`,
|
|
354
|
+
'---- stdout + stderr ----',
|
|
355
|
+
].join('\n')
|
|
356
|
+
writeFileSync(logPath, `${head}\n${String(r && r.stdout || '')}${String(r && r.stderr || '')}\n`)
|
|
357
|
+
} catch (e) {
|
|
358
|
+
logPath = null
|
|
359
|
+
console.warn(`⚠️ smoke 实录 log 落盘失败(fail-soft,判定不受影响): ${e && e.message ? e.message : e}`)
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return {
|
|
363
|
+
configured: true, status: failed ? 'failed' : 'passed', command,
|
|
364
|
+
exitCode, durationMs, logPath, ranAt,
|
|
365
|
+
timeout: timedOut, fallbackMainRepo, source: 'cli-noai-smoke',
|
|
366
|
+
reason,
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** smoke 结果一行报告(语义边界内建:失败=封顶信号非崩溃,不阻断本步) */
|
|
371
|
+
function renderSmokeReport(s) {
|
|
372
|
+
if (!s || !s.configured) {
|
|
373
|
+
return `ℹ️ smoke 冒烟:${s && s.reason ? s.reason : '未配置 commands.smoke'}`
|
|
374
|
+
}
|
|
375
|
+
const logNote = s.logPath ? `——实录 ${s.logPath}` : ''
|
|
376
|
+
if (s.status === 'passed') {
|
|
377
|
+
return `✅ smoke 冒烟通过(exit 0,${s.durationMs}ms${s.fallbackMainRepo ? ';快照超时已回退主仓复跑' : ''})${logNote}`
|
|
378
|
+
}
|
|
379
|
+
return `⚠️ smoke 冒烟失败(${s.reason || `退出码 ${s.exitCode}`})——记失败态不阻断本步(PASS 封顶信号,--done 消费)${logNote}`
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
|
|
248
383
|
/** fallback 口径可见性(2026-09-11 驾驭第十二批):快照不可用回退主仓且主仓存在非 .sillyspec
|
|
249
384
|
* 脏文件时,明示实测判定面含并行 WIP——第三撞根因是「静默 fallback 看不出走了哪个口径」。 */
|
|
250
385
|
function warnIfMainRepoDirtyForGate(cwd) {
|
|
@@ -289,6 +424,7 @@ export async function executeVerifyQualityScan({ cwd, specBase, changeName, plat
|
|
|
289
424
|
let testCheck
|
|
290
425
|
let lintCheck
|
|
291
426
|
let coverageCheck = null
|
|
427
|
+
let smokeResult = null
|
|
292
428
|
try {
|
|
293
429
|
testCheck = runVerifyTestCheck({ cwd: gateCwd, specBase: gateSpecBase, changeName })
|
|
294
430
|
printVerifyTestCheck(testCheck)
|
|
@@ -304,6 +440,16 @@ export async function executeVerifyQualityScan({ cwd, specBase, changeName, plat
|
|
|
304
440
|
console.log(`\n⏳ noAI 质量扫描:CLI 亲自实测 commands.lint…`)
|
|
305
441
|
}
|
|
306
442
|
printVerifyLintCheck(lintCheck)
|
|
443
|
+
// smoke 亲跑(2026-09-17-api-coverage-smoke task-01 / FR-01):commands.smoke 配置才执行
|
|
444
|
+
// ——失败记失败态不 throw(与 test failed 的 throw 语义刻意区分:封顶信号非崩溃,PASS
|
|
445
|
+
// 封顶消费归 --done 第五条件 / task-03)。在快照 cleanup 前跑(cwd=gateCwd 隔离快照内),
|
|
446
|
+
// 配置从主仓 local.yaml 读(与 coverage 同口径);未配置一行 ℹ️(coverage 同款噪音面)。
|
|
447
|
+
try {
|
|
448
|
+
smokeResult = runSmokeCheck({ gateCwd, mainCwd: cwd, specBase, changeName, inSnapshot: Boolean(snap) })
|
|
449
|
+
console.log(renderSmokeReport(smokeResult))
|
|
450
|
+
} catch (e) {
|
|
451
|
+
console.warn(`⚠️ smoke 冒烟执行异常(fail-soft,不影响本步推进): ${e && e.message ? e.message : e}`)
|
|
452
|
+
}
|
|
307
453
|
// P2(noai-ir-roadmap §5):coverage 存在性事实——commands.coverage 显式配置才采集,
|
|
308
454
|
// 只证「有覆盖记录」不证「覆盖了行为」(渲染自带语义边界),fail-soft 不阻断。
|
|
309
455
|
try {
|
|
@@ -315,7 +461,7 @@ export async function executeVerifyQualityScan({ cwd, specBase, changeName, plat
|
|
|
315
461
|
} finally {
|
|
316
462
|
if (snap) { try { snap.cleanup() } catch {} }
|
|
317
463
|
}
|
|
318
|
-
storeQualityScan({ specBase, cwd, changeName, testResult: testCheck, lintResult: lintCheck, coverageCheck })
|
|
464
|
+
storeQualityScan({ specBase, cwd, changeName, testResult: testCheck, lintResult: lintCheck, smokeResult, coverageCheck })
|
|
319
465
|
const testFailed = testCheck.status === 'failed'
|
|
320
466
|
const lintBlocked = shouldBlockVerifyLint(lintCheck)
|
|
321
467
|
// 摩擦计数(friction-signal-hint task-03):记录条件与 throw 条件**刻意解耦**——advisory 档
|
|
@@ -274,6 +274,20 @@ const PLAN_RULES = [
|
|
|
274
274
|
spec: 'large 变更产出 `module-impact.md` 首版(plan review_plan 步生成;scale=small 豁免)',
|
|
275
275
|
failMessage: 'module-impact.md 缺失: ${path}(large 变更应在 plan 审查计划步生成模块影响分析首版)',
|
|
276
276
|
},
|
|
277
|
+
// 全局硬约束段存在性(ql-20260917-003,warning——ql-20260917-002 落地段的事后校验):plan_level=full
|
|
278
|
+
// 的 plan.md 应含「全局硬约束」段(design.md 跨 task 硬约束逐字抄录,执行期子代理只读自己的
|
|
279
|
+
// 任务卡+本段)。condition 生效依赖 validatePlanOutputs 把 plan.md frontmatter plan_level 传入
|
|
280
|
+
// ctx;读不到(无 frontmatter/旧格式/light)→ eq:'full' 不成立跳过(存量零误报 fail-safe)。
|
|
281
|
+
// 确无跨 task 硬约束时写一段「无」占位即可消警(段在场即过,不校验段体非空——空体由 execute
|
|
282
|
+
// 注入端容忍+审查目检)。
|
|
283
|
+
{
|
|
284
|
+
id: 'plan.global-constraints', stage: 'plan', source: 'validatePlanOutputs', severity: 'warning', kind: 'literal-any',
|
|
285
|
+
target: { root: 'change', path: 'plan.md', scope: 'full' },
|
|
286
|
+
data: { literals: ['全局硬约束'] },
|
|
287
|
+
condition: { ctxField: 'planLevel', eq: 'full' },
|
|
288
|
+
spec: 'plan_level=full 的 plan.md 含「全局硬约束」段(design.md 跨 task 硬约束逐字抄录;无跨 task 硬约束时写「无」占位消警)',
|
|
289
|
+
failMessage: 'plan.md 缺少「## 全局硬约束」段(plan_level=full:从 design.md 逐字抄录绑定所有 task 的硬约束,执行期子代理只读自己的任务卡+本段;确无跨 task 硬约束时写一段「无」占位消警)',
|
|
290
|
+
},
|
|
277
291
|
// entry-point-wiring(custom):design.md 命中入口实例化/启动路径模式时,被提到的入口文件
|
|
278
292
|
// (cli.ts/main.ts/server.(js|ts)/index.(js|ts))必须出现在某 task allowed_paths 或 plan.md
|
|
279
293
|
// 文件变更清单中,否则需 design.md 紧邻写明豁免。判定算法(多源 allowed_paths 收集 + 逐文件对账 +
|