sillyspec 3.28.3 → 3.28.5

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 (77) hide show
  1. package/.claude/skills/sillyspec-auto/SKILL.md +54 -54
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -114
  3. package/.claude/skills/sillyspec-continue/SKILL.md +17 -17
  4. package/.claude/skills/sillyspec-execute/SKILL.md +222 -222
  5. package/.claude/skills/sillyspec-knowledge/SKILL.md +40 -40
  6. package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
  7. package/.claude/skills/sillyspec-propose/SKILL.md +16 -16
  8. package/.claude/skills/sillyspec-resume/SKILL.md +21 -21
  9. package/.claude/skills/sillyspec-state/SKILL.md +25 -25
  10. package/.claude/skills/sillyspec-workspace/SKILL.md +33 -33
  11. package/AGENTS.md +25 -0
  12. package/INSTRUCTIONS.md +12 -0
  13. package/package.json +1 -1
  14. package/results-review-tier.md +18 -0
  15. package/src/agent-session-log.js +4 -3
  16. package/src/archive-delta.js +82 -0
  17. package/src/change-delete.js +2 -1
  18. package/src/change-list.js +18 -3
  19. package/src/change-risk-profile.js +77 -6
  20. package/src/commit-suggest.js +52 -1
  21. package/src/config-cat.js +17 -1
  22. package/src/constants.js +3 -1
  23. package/src/decision-distill.js +45 -5
  24. package/src/decisions-io.js +96 -0
  25. package/src/design-facts.js +3 -0
  26. package/src/dispatch/probe.js +31 -8
  27. package/src/docs-check.js +102 -3
  28. package/src/doctor-diagnostics.js +441 -5
  29. package/src/foreign-declared.js +53 -2
  30. package/src/index.js +606 -22
  31. package/src/mcp-server.js +144 -0
  32. package/src/module-impact.js +84 -12
  33. package/src/modules.js +46 -34
  34. package/src/plan-adopt-waves.js +13 -5
  35. package/src/progress/change-registry.js +34 -0
  36. package/src/progress.js +75 -2
  37. package/src/quick-recommend.js +6 -1
  38. package/src/quicklog.js +70 -8
  39. package/src/review-dispatch.js +377 -0
  40. package/src/run/archive-distill.js +48 -0
  41. package/src/run/command.js +48 -13
  42. package/src/run/complete-handlers.js +215 -9
  43. package/src/run/complete.js +184 -2
  44. package/src/run/gate-snapshot.js +88 -0
  45. package/src/run/gates.js +206 -58
  46. package/src/run/prompt.js +150 -14
  47. package/src/run/quick-audit.js +38 -13
  48. package/src/run/shared.js +183 -12
  49. package/src/run/stage.js +52 -2
  50. package/src/run/verify-quality-scan.js +336 -0
  51. package/src/runtime-hygiene.js +84 -0
  52. package/src/scope-audit.js +735 -0
  53. package/src/sillyhub-mcp/client.js +58 -3
  54. package/src/spec-sync.js +63 -8
  55. package/src/stage-contract.js +83 -3
  56. package/src/stage-review-checklist.js +44 -0
  57. package/src/stage-review.js +125 -4
  58. package/src/stages/archive.js +60 -167
  59. package/src/stages/brainstorm.js +7 -17
  60. package/src/stages/doctor.js +41 -515
  61. package/src/stages/execute.js +13 -10
  62. package/src/stages/plan-postcheck.js +191 -7
  63. package/src/stages/plan.js +7 -35
  64. package/src/stages/quick.js +11 -11
  65. package/src/stages/verify.js +24 -39
  66. package/src/sync-noise.js +158 -0
  67. package/src/sync.js +93 -27
  68. package/src/task-review.js +133 -17
  69. package/src/taskcard.js +46 -3
  70. package/src/validate-artifacts.js +163 -0
  71. package/src/verify-facts-schema.js +141 -0
  72. package/src/verify-postcheck.js +347 -29
  73. package/src/verify-probes.js +275 -24
  74. package/src/workflow.js +6 -0
  75. package/src/worktree-apply.js +167 -24
  76. package/src/worktree.js +5 -1
  77. package/templates/agents-instruction.md +1 -0
@@ -11,8 +11,8 @@
11
11
  * 探针2(关键词提取半语义)/探针3.4 集成盲区/3.5 断言抽查/探针4(决策追踪语义)留 agent。
12
12
  *
13
13
  * verify-result.md 骨架:七章节固定结构 + 探针结果机械预填 + 其余章节 <!--TODO--> 占位。
14
- * 结论章节留「待填」——extractVerifyConclusion 找不到 PASS/FAIL 关键词即判不过,骨架不能
15
- * 直接过门(与 symbol-impact 骨架同款防偷懒语义)。
14
+ * 结论走「结论枚举:」固定槽行(刀③)——占位符不含枚举词,槽未填 extractVerifyConclusionSlot
15
+ * 返回 '' 即判不过,骨架不能直接过门(与 symbol-impact 骨架同款防偷懒语义)。
16
16
  * P3b 增量:①章节标题行末尾 claims 层标注(可复跑探针/确定性检查/人工判断,纯后缀不新增行);
17
17
  * ②--init 同步落盘 verify-facts.json 机器底稿(探针命令行 + 首跑关键指标 + 时间戳,CLI 全权写,
18
18
  * 供事后独立复跑审计;重复 --init 覆盖为最近一次 init 快照)。
@@ -20,7 +20,11 @@
20
20
  import { existsSync, readFileSync, readdirSync, writeFileSync } from 'fs'
21
21
  import { join, dirname, basename } from 'path'
22
22
  import { gitQuiet } from './git-helper.js'
23
+ import {
24
+ FACTS_SCHEMA_VERSION, EVIDENCE_SLOT_HEADING, RECEIPT_SLOT_HEADING, parseEvidenceSlots,
25
+ } from './verify-facts-schema.js'
23
26
  import { parseFileChangeListDetailed } from './change-list.js'
27
+ import { parseDecisions } from './decision-distill.js'
24
28
  import { parseAllowedPaths } from './stages/plan-postcheck.js'
25
29
  import { verifyApiParity, _readWorktreeMeta } from './contract-matrix.js'
26
30
  import { splitOwnVsForeignDiffFiles } from './foreign-declared.js'
@@ -149,16 +153,18 @@ export function runVerifyProbes({ cwd, changeName, specDir = null }) {
149
153
  const moduleDirs = [...new Set(allowed.map(p => dirname(p.split('\\').join('/'))).filter(d => d && d !== '.'))]
150
154
  const testFiles = []
151
155
  for (const d of moduleDirs) {
152
- // 同探针 1 的 worktree 回退(坑 probe1-worktree-path-blind):新模块目录 apply 前只在
153
- // worktree——主仓目录缺失时到 worktree 找 co-located 测试(路径相对 worktree 根呈现)
154
- let searchRoot = join(cwd, d)
155
- let relBase = cwd
156
- if (!existsSync(searchRoot) && wtRoot && existsSync(join(wtRoot, d))) {
157
- searchRoot = join(wtRoot, d)
158
- relBase = wtRoot
159
- }
160
- for (const f of findTestFiles(searchRoot, relBase)) {
161
- if (!testFiles.includes(f)) testFiles.push(f)
156
+ // 双根并集扫描(坑 probe3-worktree-test-false-negative,2026-09-10 驾驭小结第五批②,
157
+ // 用户实证 5 条假 warning):apply 前新测试文件只在 worktree(untracked),而模块目录在
158
+ // 主仓**已存在**——旧「主仓目录缺失才回退 worktree」条件不触发,探针 3 报「未找到测试
159
+ // 文件」假阴。改为无条件双根并集(主仓 ∪ worktree,各自存在才扫,路径相对各自根呈现,
160
+ // 按 rel 去重主仓优先——与探针 5 的三根并集同族)。主仓 in-place(wtRoot=null)零变化。
161
+ const roots = []
162
+ if (existsSync(join(cwd, d))) roots.push({ root: join(cwd, d), base: cwd })
163
+ if (wtRoot && wtRoot !== cwd && existsSync(join(wtRoot, d))) roots.push({ root: join(wtRoot, d), base: wtRoot })
164
+ for (const s of roots) {
165
+ for (const f of findTestFiles(s.root, s.base)) {
166
+ if (!testFiles.includes(f)) testFiles.push(f)
167
+ }
162
168
  }
163
169
  }
164
170
  probe3.tasks.push({
@@ -310,8 +316,11 @@ export function buildVerifyFacts(result, { changeName, now } = {}) {
310
316
  const p3 = (result && result.probe3) || {}
311
317
  const p5 = (result && result.probe5) || {}
312
318
  const p6 = (result && result.probe6) || {}
319
+ // v2(2026-09-08-ir-verify-facts):probes 机器段原样;conclusion/tests/requiredEvidence/
320
+ // runtimeEvidence/factsConsistency 五段是 slot-backfill/实测回填段(D-001@v2),--init 快照
321
+ // 不落键(writeVerifyFacts 分段合并时保留既有固化段),由 backfillFactsFromMdAndTests 填。
313
322
  return {
314
- schemaVersion: 1,
323
+ schemaVersion: 2,
315
324
  change: changeName,
316
325
  generatedAt: now || new Date().toISOString(),
317
326
  probes: {
@@ -350,37 +359,279 @@ export function buildVerifyFacts(result, { changeName, now } = {}) {
350
359
  }
351
360
 
352
361
  /**
353
- * verify-probes --init 落盘 verify-facts.json(CLI 全权写)。重复 --init 无条件覆盖——语义是
354
- * 「最近一次机械预填的审计底稿快照」(骨架已存在跳过时 facts 照样刷新),agent 勿手改:
355
- * 事后复跑审计的对照快照,防篡改一致性检查的对比基准是 verify-result.md 正文(非本文件)。
362
+ * 平台模式产物路径回显注记(2026-09-10 驾驭小结②,用户实锤「显示混乱」):
363
+ * verify-probes --init 在平台模式(.sillyspec-platform.json pointer 存在)下 spec 根解析为
364
+ * hub 镜像目录——回显的物理路径(骨架生成/已存在/facts 刷新)都在镜像根下,而产物经
365
+ * daemon spec-sync 落主仓 .sillyspec/changes/<change>/。agent/人工核对以主仓路径为准,
366
+ * 回显行尾追加本注记消歧义(本地模式返回空串,回显零变化)。
367
+ * @param {string|null} platformBase 平台镜像根(null/undefined = 非平台模式)
368
+ * @param {string} changeName 变更名
369
+ * @param {string} file 产物文件名(verify-result.md / verify-facts.json)
370
+ * @returns {string} 注记文本(非平台模式为 '')
371
+ */
372
+ export function formatPlatformPathNote(platformBase, changeName, file) {
373
+ if (!platformBase) return ''
374
+ return `(平台模式:物理写盘在 hub 镜像根 ${platformBase};主仓同步位置 .sillyspec/changes/${changeName}/${file},核对以主仓路径为准)`
375
+ }
376
+
377
+ /**
378
+ * verify-probes --init 落盘 verify-facts.json(CLI 全权写)。v2(2026-09-08-ir-verify-facts)改
379
+ * 分段合并:re-init 刷新机器段(probes/generatedAt),保留既有 slot-backfill/实测固化段
380
+ * (conclusion/tests/requiredEvidence/runtimeEvidence)——旧「无条件覆盖」会抹掉 --done 回填数据
381
+ * (设计审查 P1-5)。agent 勿手改:复跑审计的对照快照,防篡改基准是 verify-result.md 正文。
356
382
  * @param {string} changeDir 变更目录(spec 根下 changes/<name>)
357
383
  * @param {object} result runVerifyProbes 返回值
358
384
  * @param {string} changeName 变更名(统一命令行呈现)
385
+ * @param {{ platformNote?: string }} [opts] platformNote 回显行尾追加(平台模式路径消歧,纯回显层不进落盘)
359
386
  * @returns {{ facts: object, path: string }}
360
387
  */
361
- export function writeVerifyFacts(changeDir, result, changeName) {
362
- const facts = buildVerifyFacts(result, { changeName })
388
+ export function writeVerifyFacts(changeDir, result, changeName, opts = {}) {
389
+ const fresh = buildVerifyFacts(result, { changeName })
363
390
  const factsPath = join(changeDir, 'verify-facts.json')
391
+ let facts = fresh
392
+ try {
393
+ const prev = JSON.parse(readFileSync(factsPath, 'utf8'))
394
+ if (prev && prev.schemaVersion === FACTS_SCHEMA_VERSION) {
395
+ facts = {
396
+ ...fresh,
397
+ conclusion: prev.conclusion,
398
+ tests: prev.tests,
399
+ requiredEvidence: prev.requiredEvidence,
400
+ runtimeEvidence: prev.runtimeEvidence,
401
+ factsConsistency: prev.factsConsistency, // 保留至上次对比结论(下次 --done 重算覆盖)
402
+ }
403
+ }
404
+ } catch { /* 首次落盘/v1/损坏 → 全新快照(v1 无固化段,直接升 v2) */ }
364
405
  writeFileSync(factsPath, JSON.stringify(facts, null, 2) + '\n')
365
- console.log(`📝 已刷新 verify-facts.json 机器底稿: ${factsPath}(CLI 全权写,勿手改)`)
406
+ console.log(`📝 已刷新 verify-facts.json 机器底稿: ${factsPath}(v2 分段合并,固化段保留;CLI 全权写,勿手改)${opts.platformNote || ''}`)
366
407
  return { facts, path: factsPath }
367
408
  }
368
409
 
410
+ /**
411
+ * --done 回填(D-001@v2 slot-backfill):结论枚举槽 + 证据/回执槽解析固化进 facts;
412
+ * testCheckResult 提供时回填 tests 段(实测在 runValidators 之后 → gates 二次回填,FR-03 次序)。
413
+ * 机器段(probes/factsConsistency)不经本函数(writeVerifyFacts / checkProbeConsistency 所有)。
414
+ * fail-soft:facts 缺失时原地升 v2(probes 保留或空);落盘失败 warn 不抛。
415
+ * @param {string} factsPath verify-facts.json 路径
416
+ * @param {{ verifyMd: string, testCheckResult?: object|null }} opts
417
+ * @returns {{ facts: object, conclusion: string|null, evidenceCount: number, receiptCount: number, testsBackfilled: boolean }}
418
+ */
419
+ export function backfillFactsFromMdAndTests(factsPath, { verifyMd, testCheckResult = null, conclusion = null }) {
420
+ let facts = null
421
+ try { facts = JSON.parse(readFileSync(factsPath, 'utf8')) } catch { /* 缺失见下 */ }
422
+ // 只固化既有底稿,不无中生有(2026-09-08-ir-verify-facts 接线实证):无 facts 的存量变更
423
+ // (未跑过 --init)若被回填凭空建出 facts.json,checkProbeConsistency 的「有 facts 无探针
424
+ // 子节 → error」判别会把存量兼容 skip 误升 error(run-complete-step-verify e2e 抓出)。
425
+ // 底稿创建唯一入口是 verify-probes --init。
426
+ if (!facts) {
427
+ return { facts: null, skipped: true, conclusion: null, evidenceCount: 0, receiptCount: 0, testsBackfilled: false, reason: '无 verify-facts.json(未跑 --init),回填跳过——底稿创建唯一入口是 --init' }
428
+ }
429
+ if (facts.schemaVersion !== FACTS_SCHEMA_VERSION) {
430
+ facts = { ...facts, schemaVersion: FACTS_SCHEMA_VERSION, probes: (facts && facts.probes) || {} }
431
+ }
432
+ const slots = parseEvidenceSlots(verifyMd || '')
433
+ // conclusion 由调用方传 stage-contract.extractVerifyConclusionSlot(verifyMd) 的结果
434
+ // (避免 verify-probes → stage-contract 静态 import 的潜在环,分层单向)
435
+ const conclusionSlot = conclusion || null
436
+ if (conclusionSlot) facts.conclusion = conclusionSlot
437
+ if (slots.hasEvidenceSlot && slots.requiredEvidence.length > 0) {
438
+ facts.requiredEvidence = slots.requiredEvidence.map(e => ({
439
+ task: e.task, status: e.status, verifiedFiles: e.verifiedFiles,
440
+ ...(e.exempt ? { exempt: true, exemptionReason: e.exemptionReason } : {}),
441
+ }))
442
+ }
443
+ if (slots.hasReceiptSlot && slots.runtimeEvidence.length > 0) {
444
+ facts.runtimeEvidence = slots.runtimeEvidence
445
+ }
446
+ let testsBackfilled = false
447
+ if (testCheckResult && testCheckResult.status && testCheckResult.status !== 'skipped') {
448
+ facts.tests = {
449
+ command: testCheckResult.command || null,
450
+ exitCode: typeof testCheckResult.exitCode === 'number' ? testCheckResult.exitCode : null,
451
+ strategy: testCheckResult.mode || testCheckResult.strategy || null,
452
+ failedRemaining: testCheckResult.failureNames || testCheckResult.failedTests || [],
453
+ passedAt: new Date().toISOString(),
454
+ }
455
+ testsBackfilled = true
456
+ }
457
+ try {
458
+ writeFileSync(factsPath, JSON.stringify(facts, null, 2) + '\n')
459
+ } catch (e) {
460
+ console.warn(`⚠️ facts 回填落盘失败(fail-soft,不阻断): ${e && e.message ? e.message : e}`)
461
+ }
462
+ return { facts, conclusion: conclusionSlot, evidenceCount: slots.requiredEvidence.length, receiptCount: slots.runtimeEvidence.length, testsBackfilled }
463
+ }
464
+
465
+ /**
466
+ * md 槽段缺失补齐(--init 段落级补齐,2026-09-08-ir-verify-facts R-02):已存在的
467
+ * verify-result.md 缺「证据账/集成验证回执」槽段时仅追加骨架(不触碰既有正文),幂等二跑零改动。
468
+ * @param {string} mdPath verify-result.md 路径
469
+ * @param {Array<{task:string,evidence:string[]}>} [requiredEvidenceItems] verify-required-evidence.json items(预填 task 行)
470
+ * @returns {{ added: string[] }} 追加的槽段标题列表
471
+ */
472
+ export function backfillMissingEvidenceSlots(mdPath, requiredEvidenceItems = []) {
473
+ let text = ''
474
+ try { text = readFileSync(mdPath, 'utf8') } catch { return { added: [] } }
475
+ const normalized = text.replace(/\r\n/g, '\n')
476
+ const added = []
477
+ const blocks = []
478
+ if (!normalized.includes(EVIDENCE_SLOT_HEADING)) {
479
+ const lines = [EVIDENCE_SLOT_HEADING, '[层:人工判断——CLI 核验]', '', '<!-- 无 cannot_verify 任务时本节写「无」 -->']
480
+ if (requiredEvidenceItems.length > 0) {
481
+ for (const it of requiredEvidenceItems) {
482
+ lines.push(`- ${it.task}: <待填:三选一> | verifiedFiles: <精确路径,逗号分隔>(satisfied 必填;豁免时填 missing 并加(豁免:<一句话理由>)后缀)`)
483
+ }
484
+ } else {
485
+ lines.push('无(本次变更无 cannot_verify 任务)')
486
+ }
487
+ lines.push('')
488
+ blocks.push(lines.join('\n'))
489
+ added.push(EVIDENCE_SLOT_HEADING)
490
+ }
491
+ if (!normalized.includes(RECEIPT_SLOT_HEADING)) {
492
+ blocks.push([
493
+ RECEIPT_SLOT_HEADING, '[层:自述声明——CLI 一致性校验]', '',
494
+ '<!-- integration-critical/deployment-critical 变更必填;其余写「无」 -->',
495
+ '- claim: <待填:一句话> | command: <待填:命令> | exit: <待填:0 或非 0> | log: <待填:日志路径>',
496
+ '',
497
+ ].join('\n'))
498
+ added.push(RECEIPT_SLOT_HEADING)
499
+ }
500
+ if (added.length > 0) {
501
+ writeFileSync(mdPath, normalized.replace(/\n?$/, '\n') + blocks.join('\n') + '\n')
502
+ }
503
+ return { added }
504
+ }
505
+
506
+ /**
507
+ * 结论草稿槽行预填(P0-1 前置三,noai-ir-roadmap §3):机械事实全绿时把骨架「结论枚举:」
508
+ * 待填槽替换为 PASS 草稿——显式「草稿待确认」内联标注(非默认值语义),agent 复核后可改写;
509
+ * gate 对槽行的独立复核逻辑不经本函数(判定链不动,预填只省打字成本)。纯文本变换三态:
510
+ * 槽行未填(仍含「待填」)→ 改写;已填(任何枚举值)→ 原样返回;无槽行(legacy 存量)→
511
+ * 原样返回。改写后槽行满足 extractVerifyConclusionSlot 行首锚定枚举解析(返回 PASS)。
512
+ */
513
+ export function applyConclusionDraftToText(text, draft = 'PASS') {
514
+ const normalized = String(text).replace(/\r\n/g, '\n')
515
+ const lines = normalized.split('\n')
516
+ for (let i = 0; i < lines.length; i++) {
517
+ if (/^结论枚举:/.test(lines[i]) && lines[i].includes('待填')) {
518
+ lines[i] = `结论枚举:${draft}(🤖 CLI 草稿待确认——机械事实全绿自动预填:noAI 实测通过 + 探针 1/3/5/6 干净 + 风险门非 integration/deployment;复核后不同意即改写本行枚举值,gate 独立复核不受预填影响)`
519
+ return lines.join('\n')
520
+ }
521
+ }
522
+ return text
523
+ }
524
+
525
+ /**
526
+ * 决策追踪矩阵机械半边(P0-1,noai-ir-roadmap §3):D→FR→task 链自结构化字段构建——
527
+ * decisions.md 条目(parseDecisions 双格式解析,与 distill 同源单一实现)× tasks/task-NN.md
528
+ * frontmatter 的 decision_ids / requirement_ids(taskcard 9 硬校验字段,CLI 骨架写入——比
529
+ * tasks.md 行内注解可靠)。Evidence / 状态两列是人工判断置 <待填>;D 无 task 回指 →
530
+ * ⚠️ 未闭环显式列出(这正是矩阵要暴露的风险,不静默)。
531
+ * @returns {{ rows: string[], decisionCount: number, taskCount: number }|null}
532
+ * null = 无 decisions.md / 解析 0 条(矩阵段留 TODO 不注入)
533
+ */
534
+ export function buildDecisionChainMatrix(changeDir) {
535
+ let decisions
536
+ try {
537
+ decisions = parseDecisions(changeDir)
538
+ } catch {
539
+ return null
540
+ }
541
+ if (!decisions || decisions.missing || (decisions.entries || []).length === 0) return null
542
+ // task 卡 frontmatter 结构化字段反查(列表 [a, b] 形态;frontmatter 由 plan-postcheck 硬校验)
543
+ const taskRefs = []
544
+ const tasksDir = join(changeDir, 'tasks')
545
+ try {
546
+ if (existsSync(tasksDir)) {
547
+ for (const f of readdirSync(tasksDir).filter((f) => /^task-\d+\.md$/.test(f)).sort()) {
548
+ const raw = readFileSync(join(tasksDir, f), 'utf8')
549
+ const id = (raw.match(/^id:\s*(\S+)/m) || [])[1] || f.replace(/\.md$/, '')
550
+ const decLine = (raw.match(/^decision_ids:\s*\[([^\]]*)\]/m) || [])[1] || ''
551
+ const reqLine = (raw.match(/^requirement_ids:\s*\[([^\]]*)\]/m) || [])[1] || ''
552
+ taskRefs.push({
553
+ task: id,
554
+ decisions: new Set(decLine.split(',').map((s) => s.trim()).filter(Boolean)),
555
+ reqs: new Set(reqLine.split(',').map((s) => s.trim()).filter(Boolean)),
556
+ })
557
+ }
558
+ }
559
+ } catch { /* tasks 目录不可读 → 矩阵 task 列全标未闭环(信息仍在) */ }
560
+ const rows = []
561
+ for (const d of decisions.entries) {
562
+ // task 卡 decision_ids 可能写 D-001(无 @vN)——id 与 number 双匹配同源
563
+ const hitTasks = taskRefs.filter((t) => t.decisions.has(d.id) || t.decisions.has(d.number))
564
+ const frs = [...new Set(hitTasks.flatMap((t) => [...t.reqs]))].sort()
565
+ const taskCell = hitTasks.length > 0 ? hitTasks.map((t) => t.task).join('、') : '⚠️ 未闭环(无 task 回指)'
566
+ const frCell = frs.length > 0 ? frs.join('、') : '⚠️ 未映射'
567
+ rows.push(`| ${d.id} | ${frCell} | ${taskCell} | <待填:证据回指> | <待填> |`)
568
+ }
569
+ return { rows, decisionCount: decisions.entries.length, taskCount: taskRefs.length }
570
+ }
571
+
572
+ /**
573
+ * 矩阵机械半边注入 verify-result.md(--init 接线):只在「决策追踪矩阵」段内仍是骨架
574
+ * TODO 行(含 D-xxx 形态)且段内无既有表格时替换注入——幂等(agent 已写/前次注入零改动),
575
+ * 不触碰正文其余部分。落盘失败 fail-soft 返回 null。
576
+ * @returns {{ decisions: number, tasks: number }|null} null = 无 decisions/无可替换 TODO/已注入
577
+ */
578
+ export function injectDecisionChainDraft(mdPath, changeDir) {
579
+ let text
580
+ try { text = readFileSync(mdPath, 'utf8') } catch { return null }
581
+ const headingIdx = text.indexOf('## 决策追踪矩阵')
582
+ if (headingIdx === -1) return null
583
+ const nextHeading = text.indexOf('\n## ', headingIdx + 1)
584
+ const sectionEnd = nextHeading === -1 ? text.length : nextHeading
585
+ const section = text.slice(headingIdx, sectionEnd)
586
+ if (/^\|/m.test(section)) return null // 已有表格 → 尊重既有内容
587
+ const todoRe = /<!--TODO:[^\n]*D-xxx[^\n]*-->/
588
+ if (!todoRe.test(section)) return null // 无骨架 TODO(手写正文)→ 不动
589
+ const matrix = buildDecisionChainMatrix(changeDir)
590
+ if (!matrix || matrix.rows.length === 0) return null
591
+ const block = [
592
+ '<!-- 机械半边预填(CLI,P0-1):D→FR→task 链自 decisions.md × tasks/*.md frontmatter 结构化字段构建;',
593
+ ' Evidence / 状态两列是人工判断——逐格复核,未闭环行必须在报告标注风险 -->',
594
+ '| 决策 ID | FR | Task | Evidence | 状态 |',
595
+ '|---|---|---|---|---|',
596
+ ...matrix.rows,
597
+ ].join('\n')
598
+ const next = text.slice(0, headingIdx) + section.replace(todoRe, block) + text.slice(sectionEnd)
599
+ try {
600
+ writeFileSync(mdPath, next)
601
+ } catch (e) {
602
+ console.warn(`⚠️ 决策追踪矩阵预填落盘失败(fail-soft): ${e && e.message ? e.message : e}`)
603
+ return null
604
+ }
605
+ return { decisions: matrix.decisionCount, tasks: matrix.taskCount }
606
+ }
607
+
369
608
  /**
370
609
  * 生成 verify-result.md 骨架(七章节;探针结果机械预填,语义章节 <!--TODO--> 占位)。
371
- * 结论章节留「待填」——extractVerifyConclusion 无 PASS/FAIL 关键词即判不过,骨架不能直接过门。
610
+ * 结论走「结论枚举:」固定槽行(刀③,2026-09-08)——占位符不含枚举词(<待填:三选一>),
611
+ * 槽未填 → extractVerifyConclusionSlot 返回 '' → gate 判不过(fail-closed;旧占位符
612
+ * `<待填:PASS 或 FAIL>` 含 PASS 字样会被窗口正则误读成已填 PASS,已修)。
372
613
  * 章节标题行末尾带 claims 层标注(P3b):探针结果=可复跑探针 / 测试结果=确定性检查 / 其余=
373
- * 人工判断——纯渲染层后缀,不新增行,verify gate 结论提取按关键词窗口制不受影响。
614
+ * 人工判断——纯渲染层后缀,不新增行;结论解析槽优先于标题关键词窗口(存量兼容)。
374
615
  * @returns {string|null} 骨架全文;无 design.md/tasks.md(非完整流程变更)→ null
375
616
  */
376
617
  export function generateVerifyResultSkeleton(result) {
377
618
  const L = [
378
619
  '# 验证报告(骨架由 `sillyspec verify-probes --change <变更名> --init` 生成)',
379
620
  '',
380
- '> 探针结果已机械预填;其余章节把 `<!--TODO-->` 替换为真实内容。**结论必须写明 PASS / FAIL**——',
381
- '> 留「待填」会被 gate 判不过(fail-closed)。',
621
+ '> 探针结果已机械预填;其余章节把 `<!--TODO-->` 替换为真实内容。**结论只认「结论枚举:」槽行**——',
622
+ '> 槽行留「<待填:三选一>」会被 gate 判不过(fail-closed),正文其他位置的 PASS/FAIL 字样不参与判定。',
623
+ '',
624
+ '## 结论 [层:人工判断]',
625
+ '',
626
+ '结论枚举:`<待填:三选一>`(把尖括号占位整体替换为 PASS / PASS WITH NOTES / FAIL 之一;一句话理由写在枚举后同行或下一行)',
627
+ '',
628
+ '## 证据账(cannot_verify 任务) [层:人工判断——CLI 核验]',
629
+ '<!-- 无 cannot_verify 任务时本节写「无」;有则逐 task 一行 -->',
630
+ '- task-NN: <待填:三选一> | verifiedFiles: <精确路径,逗号分隔>(satisfied 必填;豁免时填 missing 并加(豁免:<一句话理由>)后缀)',
382
631
  '',
383
- '## 结论:<待填:PASS 或 FAIL(+一句话理由)> [层:人工判断]',
632
+ '## 集成验证回执 [层:自述声明——CLI 一致性校验]',
633
+ '<!-- integration-critical/deployment-critical 变更必填;其余写「无」 -->',
634
+ '- claim: <待填:一句话> | command: <待填:命令> | exit: <待填:0 或非 0> | log: <待填:日志路径>',
384
635
  '',
385
636
  '## 任务完成度 [层:人工判断]',
386
637
  '<!--TODO: 逐 task 对照 tasks.md 勾选与验收标准,完成/未完成/存疑三态-->',
package/src/workflow.js CHANGED
@@ -12,6 +12,7 @@
12
12
  import { readFileSync, existsSync, readdirSync, writeFileSync, mkdirSync } from 'fs'
13
13
  import { join, resolve, basename } from 'path'
14
14
  import jsYaml from 'js-yaml'
15
+ import { pruneTimestampedEntries } from './runtime-hygiene.js'
15
16
  import { WORKFLOW_STATUS } from './constants.js'
16
17
  import { contentNonEmpty, lineCount, missingSections, placeholderLineMatches } from './check-primitives.js'
17
18
  import { collectInvalidDocRefs } from './docs-check.js'
@@ -626,6 +627,11 @@ export function saveWorkflowRun(result, options = {}) {
626
627
 
627
628
  try {
628
629
  writeFileSync(filepath, JSON.stringify(record, null, 2), 'utf8')
630
+ // 写入侧滚动裁剪(runtime-hygiene.js,2026-09-08 系统性回收):workflow-runs/ 只写不收,
631
+ // 实证本仓 21 份累积。文件名以零填充时间戳开头,name 字典序 == 时间序。fail-open 静默。
632
+ try {
633
+ pruneTimestampedEntries({ dir: runDir, keep: 30 })
634
+ } catch { /* 卫生动作 fail-open */ }
629
635
  return filepath
630
636
  } catch (e) {
631
637
  console.warn('⚠️ 保存 workflow run 失败:', e.message)