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.
Files changed (52) hide show
  1. package/.claude/skills/sillyspec-execute/SKILL.md +223 -222
  2. package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
  3. package/.claude/skills/sillyspec-quick/SKILL.md +8 -10
  4. package/.claude/skills/sillyspec-verify/SKILL.md +91 -87
  5. package/.husky/pre-commit +4 -0
  6. package/.husky/pre-push +3 -0
  7. package/AGENTS.md +1 -1
  8. package/package.json +1 -1
  9. package/src/change-risk-profile.js +76 -6
  10. package/src/commit-guard.js +119 -0
  11. package/src/config-schema.js +23 -0
  12. package/src/design-facts.js +46 -9
  13. package/src/diagnostic-codes.js +95 -0
  14. package/src/docs-check.js +14 -10
  15. package/src/docs-debt.js +7 -2
  16. package/src/docs-gate.js +42 -15
  17. package/src/doctor-diagnostics.js +141 -1
  18. package/src/endpoint-extractor.js +50 -2
  19. package/src/foreign-declared.js +25 -16
  20. package/src/hooks/worktree-guard.js +75 -2
  21. package/src/index.js +129 -1
  22. package/src/machine-interface.js +63 -0
  23. package/src/probe7-anchor-check.js +20 -2
  24. package/src/progress/stage-machine.js +34 -5
  25. package/src/progress.js +1 -1
  26. package/src/quicklog.js +231 -1
  27. package/src/run/complete-handlers.js +172 -1
  28. package/src/run/complete.js +73 -3
  29. package/src/run/gate-snapshot.js +262 -0
  30. package/src/run/gates.js +41 -7
  31. package/src/run/prompt.js +79 -1
  32. package/src/run/shared.js +10 -0
  33. package/src/run/verify-quality-scan.js +151 -5
  34. package/src/stage-contract-spec.js +14 -0
  35. package/src/stage-contract.js +738 -8
  36. package/src/stage-review-checklist.js +16 -2
  37. package/src/stage-review.js +137 -2
  38. package/src/stages/archive.js +9 -5
  39. package/src/stages/brainstorm.js +7 -1
  40. package/src/stages/execute.js +62 -8
  41. package/src/stages/plan-postcheck.js +42 -5
  42. package/src/stages/plan.js +8 -1
  43. package/src/stages/quick.js +5 -5
  44. package/src/stages/verify.js +8 -3
  45. package/src/sync.js +69 -0
  46. package/src/task-review.js +9 -3
  47. package/src/verify-facts-schema.js +95 -10
  48. package/src/verify-postcheck.js +346 -28
  49. package/src/verify-probes.js +1221 -46
  50. package/src/worktree-apply.js +175 -3
  51. package/src/write-audit.js +55 -0
  52. 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 收集 + 逐文件对账 +