sillyspec 3.26.7 → 3.26.9

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 (62) hide show
  1. package/.claude/skills/sillyspec-archive/SKILL.md +1 -2
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +1 -2
  3. package/.claude/skills/sillyspec-doctor/SKILL.md +2 -0
  4. package/.claude/skills/sillyspec-execute/SKILL.md +10 -2
  5. package/.claude/skills/sillyspec-explore/SKILL.md +0 -1
  6. package/.claude/skills/sillyspec-plan/SKILL.md +1 -2
  7. package/.claude/skills/sillyspec-quick/SKILL.md +1 -2
  8. package/.claude/skills/sillyspec-scan/SKILL.md +2 -2
  9. package/.claude/skills/sillyspec-status/SKILL.md +0 -1
  10. package/.claude/skills/sillyspec-verify/SKILL.md +1 -2
  11. package/.husky/pre-push +9 -2
  12. package/README.md +5 -9
  13. package/bin/sillyspec.js +1 -1
  14. package/package.json +2 -2
  15. package/packages/dashboard/server/index.js +14 -8
  16. package/src/change-risk-profile.js +5 -207
  17. package/src/config-schema.js +310 -287
  18. package/src/constants.js +97 -106
  19. package/src/db-engine.js +1 -1
  20. package/src/docs-check.js +362 -0
  21. package/src/docs-debt.js +226 -0
  22. package/src/docs-gate.js +106 -0
  23. package/src/doctor-diagnostics.js +23 -2
  24. package/src/git-helper.js +2 -2
  25. package/src/hooks/worktree-guard.js +1 -1
  26. package/src/index.js +1963 -1787
  27. package/src/init.js +95 -17
  28. package/src/machine-interface.js +7 -4
  29. package/src/modules.js +490 -486
  30. package/src/progress/stage-machine.js +3 -0
  31. package/src/progress.js +37 -13
  32. package/src/quicklog.js +182 -1
  33. package/src/run/command.js +154 -26
  34. package/src/run/complete-handlers.js +1254 -1128
  35. package/src/run/complete.js +9 -33
  36. package/src/run/gates.js +85 -1
  37. package/src/run/prompt.js +82 -1
  38. package/src/run/quick-audit.js +23 -0
  39. package/src/run/shared.js +273 -5
  40. package/src/run/stage.js +17 -1
  41. package/src/scan-diff.js +388 -0
  42. package/src/scan-staleness.js +95 -0
  43. package/src/stage-contract-spec.js +16 -1
  44. package/src/stage-contract.js +8 -5
  45. package/src/stage-review.js +1 -1
  46. package/src/stages/brainstorm-auto.js +0 -1
  47. package/src/stages/brainstorm.js +3 -3
  48. package/src/stages/execute.js +35 -11
  49. package/src/stages/plan-postcheck.js +93 -36
  50. package/src/stages/plan.js +22 -7
  51. package/src/stages/quick.js +1 -1
  52. package/src/stages/scan.js +2 -2
  53. package/src/stages/verify.js +11 -12
  54. package/src/sync.js +39 -17
  55. package/src/task-review.js +14 -5
  56. package/src/workflow.js +8 -185
  57. package/src/worktree-apply.js +128 -3
  58. package/src/worktree.js +2 -2
  59. package/templates/claude-instruction.md +1 -1
  60. package/templates/prompts/taskcard-rules.md +1 -0
  61. package/templates/prompts/verify-probes.md +2 -2
  62. package/templates/skills/sillyspec-onboard/SKILL.md +0 -92
@@ -24,7 +24,7 @@ import { triggerSync, WAIT_MARKER_RE, getStageSteps, formatWaitOptions, resolveR
24
24
  import { executeScanPreflight, executeScanPostcheck, computeScanProfile } from './scan-profile.js'
25
25
  import { executePlanPostcheck as runPlanPostcheckLib } from '../stages/plan-postcheck.js'
26
26
  import { outputStep } from './prompt.js'
27
- import { enforceDepsGate, enforceReviewJsonGate, completeStageGates, readDesignScale } from './gates.js'
27
+ import { enforceDepsGate, enforceReviewJsonGate, enforceSymbolImpactGate, completeStageGates, readDesignScale } from './gates.js'
28
28
  import { handleArchiveConfirmStep, handlePlanGeneratePlanStep, handleScanProjectListStep, handleWorkflowPostCheck, handleQuickStageCompletion, handleExecuteWaveArtifact } from './complete-handlers.js'
29
29
  import { formatExecuteSummary } from '../worktree-apply.js'
30
30
  import { isEndToEndTaskText } from '../change-risk-profile.js'
@@ -64,32 +64,6 @@ export function resolveWaitingStepWithAnswer(steps, doneAnswer, nowStr) {
64
64
  return waitIdx
65
65
  }
66
66
 
67
- /**
68
- * wait 选项单选校验(坑 wait-choice-enforcement):定义了 waitOptions 的 requiresWait/
69
- * repeatableWait 步骤,--answer 必须命中预设选项之一——防止 agent 一句话代答绕过人工
70
- * 选择(方案选择类 wait 的 answer 本就该是选项本身)。开放回答型步骤(澄清追问,answer
71
- * 为自由文本)在 step 定义显式声明 waitFreeAnswer: true 豁免。
72
- * 校验失败打印选项清单 + exit 1(fail-loud,与 requiresWait 门同风格)。
73
- *
74
- * waitOptions 优先取 stepDef(运行时构建的步骤定义,与 brainstorm/plan 静态定义同源),
75
- * 回退 DB step entry(--continue 路径 waiting 步骤的 waitOptions 已落盘)。
76
- */
77
- function enforceWaitChoice(stepDef, stepEntry, answer, stageName, changeName) {
78
- if (answer == null) return
79
- const opts = Array.isArray(stepDef?.waitOptions) ? stepDef.waitOptions
80
- : (Array.isArray(stepEntry?.waitOptions) ? stepEntry.waitOptions : null)
81
- if (!opts || opts.length === 0) return
82
- if (stepDef?.waitFreeAnswer === true) return
83
- if (opts.includes(answer)) return
84
- console.error(`❌ Step "${stepEntry?.name || '当前步骤'}" 是封闭单选 wait,--answer 必须命中预设选项之一(防 AI 代答绕过人工选择)。`)
85
- console.error(` 你的 --answer: ${JSON.stringify(answer)}`)
86
- console.error(` 可选值(复制其一): ${opts.join(' / ')}`)
87
- console.error(` 自由文本回答的场景,该步骤应声明 waitFreeAnswer(当前未声明)。`)
88
- if (changeName) console.error(` 重试:sillyspec run ${stageName} --done --answer "<选项>" --change ${changeName}`)
89
- else console.error(` 重试:sillyspec run ${stageName} --done --answer "<选项>"`)
90
- process.exit(1)
91
- }
92
-
93
67
  export async function completeStep(pm, progress, stageName, cwd, outputText, inputText = null, options = {}) {
94
68
  const { printNext = true, confirm = false, changeName, platformOpts = {}, nonInteractive = false, isForceBaseline = false, isAllowNew = false, isAllowDelete = false } = options
95
69
  const specBase = platformOpts.specRoot || join(cwd, '.sillyspec')
@@ -148,10 +122,6 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
148
122
  const defStepsForCurrent = await getStageSteps(stageName, cwd, progress, platformOpts?.specRoot || null)
149
123
  const currentStepDef = defStepsForCurrent?.[currentIdx] || {}
150
124
  const currentStep = steps[currentIdx]
151
- // --done --answer 解 waiting 的路径同样过单选校验(与下方 requiresWait doneAnswer 分支同规则)
152
- if (_resolvedWaitIdx !== -1 && _doneAnswer) {
153
- enforceWaitChoice(currentStepDef, currentStep, _doneAnswer, stageName, changeName)
154
- }
155
125
  if (currentStepDef.requiresWait === true && !currentStep.waitAnswer) {
156
126
  // 检查 --done 是否带了 --answer:如果是,自动补全 waitAnswer 状态,一步完成
157
127
  const doneAnswer = typeof options !== 'undefined' && options.doneAnswer ? options.doneAnswer : null
@@ -168,7 +138,6 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
168
138
  currentStep.waitReason = currentStepDef.waitReason || '等待用户输入'
169
139
  console.log(`⚠️ Step "${currentStep.name}" 的确认(${currentStepDef.waitReason})已在前置步骤完成,自动跳过。`)
170
140
  } else if (doneAnswer) {
171
- enforceWaitChoice(currentStepDef, currentStep, doneAnswer, stageName, changeName)
172
141
  currentStep.status = 'waiting'
173
142
  currentStep.waitAnswer = doneAnswer
174
143
  currentStep.waitReason = currentStepDef.waitReason || '等待用户输入'
@@ -191,6 +160,9 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
191
160
  // review.json 字段硬门(坑 review-json-field-gap):已勾 [x] task 的 review.json 必须 schema 完整,
192
161
  // 提前到每次 --done 校验(而非等 Task Review Gate 在整阶段收尾才暴露,迫使用户事后批量补)。
193
162
  await enforceReviewJsonGate(stageName, cwd, changeName, steps[currentIdx], steps, currentIdx, specBase, platformOpts)
163
+ // 符号影响面报告硬门(ql-20260816-005-3d7f):execute「加载上下文」步产出落盘核验——
164
+ // symbol-impact.md 存在 + plan 每 task 有结论行;防前缀步被一句「上下文在会话内」盖章跳过。
165
+ await enforceSymbolImpactGate(stageName, changeName, steps[currentIdx]?.name, specBase)
194
166
 
195
167
  // ── noAI 步骤硬门(坑 noai-done-bypass):noAI 步骤的确定性校验不可被 --done 绕过 ──
196
168
  // 正常路径 agent 跑 `run <stage>` 推进到 noAI step 时,runStage 自动执行 _cliAction
@@ -354,6 +326,9 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
354
326
  console.warn('⚠️ 阶段完成 ctx 构造失败,降级单仓 gate(' + (e && e.message ? e.message : e) + ')')
355
327
  }
356
328
  const _stageGatesResult = await completeStageGates({ stageName, cwd, changeName, platformOpts, specBase, progress, pm, stageData, steps, currentIdx, outputText, ctx: _completeCtx })
329
+ // task-04 / A5:gate 失败(stageCompleted===false)设进程退出码 1,对齐 quick 审计 blocked→exit 1 惯例。
330
+ // 用 process.exitCode 而非 process.exit(1):让回滚 pm._write + triggerSync 落盘完成后自然退出,保留失败现场。
331
+ if (_stageGatesResult?.stageCompleted === false) process.exitCode = 1
357
332
  if (_stageGatesResult) return _stageGatesResult
358
333
 
359
334
  // 完整流程 change title 刷新:proposal/design 落盘后(brainstorm/plan 完成),从 proposal 首个 #
@@ -750,7 +725,6 @@ export async function continueStep(pm, progress, stageName, cwd, answer, options
750
725
  const prevOutput = currentStep.output || ''
751
726
  const waitRound = (currentStep.waitRound || 0) + 1
752
727
  currentStep.waitRound = waitRound
753
- enforceWaitChoice(currentStepDef, currentStep, answer, stageName, changeName)
754
728
  currentStep.waitAnswer = answer
755
729
  currentStep.waitAnswers = Array.isArray(currentStep.waitAnswers) ? currentStep.waitAnswers : []
756
730
  currentStep.waitAnswers.push({
@@ -837,6 +811,8 @@ export async function continueStep(pm, progress, stageName, cwd, answer, options
837
811
  console.warn('⚠️ 阶段完成 ctx 构造失败,降级单仓 gate(' + (e && e.message ? e.message : e) + ')')
838
812
  }
839
813
  const _stageGatesResult = await completeStageGates({ stageName, cwd, changeName, platformOpts, specBase, progress, pm, stageData, steps: stageData.steps, currentIdx, outputText: null, ctx: _contCtx })
814
+ // task-04 / A5:gate 失败(stageCompleted===false)设进程退出码 1(与 completeStep 完成分支同语义)。
815
+ if (_stageGatesResult?.stageCompleted === false) process.exitCode = 1
840
816
  if (_stageGatesResult) return _stageGatesResult
841
817
  // 完整流程 change title 刷新(同 completeStep 完成分支,wait 解除后阶段完成也刷新)。
842
818
  if (changeName && !/^quick-[0-9a-f]{8}$/.test(changeName)) {
package/src/run/gates.js CHANGED
@@ -27,6 +27,68 @@ import { handleScanStageCompleted, handleExecuteWorktreeCleanup } from './comple
27
27
  import { detectConcurrentChanges, formatConcurrentWarning } from './concurrent-detect.js'
28
28
  import { stageRegistry } from '../stages/index.js'
29
29
 
30
+ /**
31
+ * 从 plan.md 提取全部 task id(task-XX)——符号影响面覆盖度校验用。
32
+ * 只认 checkbox 行 `- [ ] task-XX:` / `- [x] task-XX:`(与 execute.js parseWavesFromPlan 的
33
+ * 收容口径一致:### 标题式 task / 非 task 区 checkbox 不算),light plan 的 `## Tasks` 段同样收。
34
+ */
35
+ function extractTaskIdsFromPlan(planContent) {
36
+ if (!planContent) return []
37
+ return [...String(planContent).matchAll(/^[-*]\s*\[[ x]\]\s*(?:.*\b)?task-(\d+)\b/gim)]
38
+ .map(m => `task-${m[1].padStart(2, '0')}`)
39
+ }
40
+
41
+ /**
42
+ * 符号影响面报告结构校验(纯函数):execute「加载上下文」步的产出落盘核验。
43
+ *
44
+ * 治 persuasion-only 失效现场(ql-20260816-005-3d7f):execute 前缀 4 步无任何 gate,agent 可
45
+ * <1s 连发 4 次 --done 盖章跳过,「符号影响面扫描」(execute.js 操作 11)被一句「上下文在会话内」
46
+ * 带过(输出契约只写「上下文摘要」,操作与输出脱节)。修法 = 报告落盘
47
+ * {SPEC_ROOT}/changes/<change>/symbol-impact.md + 本函数校验:plan.md 每个 task-XX 在报告中出现
48
+ * (逐 task 结论,「无签名级变更」也要显式写,治整体一句话带过)。语义质量(调用点找没找全)
49
+ * 仍归 agent,CLI 只核结构覆盖度——符合债单「persuasion-only → 补最小硬门」原则。
50
+ *
51
+ * @param {{reportContent: string|null, planContent: string|null}} params
52
+ * @returns {{ok: boolean, errors: string[]}}
53
+ */
54
+ export function validateSymbolImpactReport({ reportContent, planContent }) {
55
+ const errors = []
56
+ if (!reportContent || !String(reportContent).trim()) {
57
+ return { ok: false, errors: ['symbol-impact.md 不存在或内容为空——「加载上下文」步的符号影响面报告必须落盘(逐 task 一行结论,「无签名级变更」也要显式写)'] }
58
+ }
59
+ const taskIds = extractTaskIdsFromPlan(planContent)
60
+ if (taskIds.length === 0) return { ok: true, errors } // plan 无 task 可列(默认兜底形态)→ 无可校验对象
61
+ const missing = taskIds.filter(id => !String(reportContent).includes(id))
62
+ for (const id of missing) {
63
+ errors.push(`${id} 未在 symbol-impact.md 中出现——每个 task 都要有一行结论(含「无签名级变更」的显式声明)`)
64
+ }
65
+ return { ok: errors.length === 0, errors }
66
+ }
67
+
68
+ /**
69
+ * 符号影响面报告硬门:execute「加载上下文」步 --done 时校验 symbol-impact.md 落盘 + task 覆盖度。
70
+ * fail-closed:不通过 → 当前步标 blocked + exit 1(进度不推进)。非目标步骤/阶段直接放行。
71
+ */
72
+ export async function enforceSymbolImpactGate(stageName, changeName, currentStepName, specBase) {
73
+ if (stageName !== 'execute') return
74
+ if (!currentStepName || !currentStepName.includes('加载上下文')) return
75
+ const planPath = join(specBase, 'changes', changeName || '', 'plan.md')
76
+ const reportPath = join(specBase, 'changes', changeName || '', 'symbol-impact.md')
77
+ if (!existsSync(planPath)) return // plan.md 缺失场景已有 plan 阶段 gate 把关,此处不重复拦
78
+ let planContent = ''
79
+ try { planContent = readFileSync(planPath, 'utf8') } catch { return }
80
+ let reportContent = null
81
+ try { reportContent = readFileSync(reportPath, 'utf8') } catch { /* 缺失由 validate 报 */ }
82
+ const result = validateSymbolImpactReport({ reportContent, planContent })
83
+ if (result.ok) return
84
+ console.error('❌ ── 符号影响面报告校验阻断(本次 --done 未完成,进度未推进)──')
85
+ console.error(` 「加载上下文」步的符号影响面检查报告未落盘或不完整:`)
86
+ console.error(` 期望路径:${reportPath}`)
87
+ for (const e of result.errors) console.error(` • ${e}`)
88
+ console.error(' 修复:按「加载上下文」步操作 11 完成符号影响面扫描,逐 task 写结论(无签名级变更也要显式写「无」),落盘后重跑 execute --done。')
89
+ process.exit(1)
90
+ }
91
+
30
92
  /**
31
93
  * 判断当前 execute step 所在 wave 是否全部 task 都声明 no_deps_verify: true(D-006@v2)。
32
94
  * 仅 wave 执行步骤(名如 "Wave N 执行")可 opt-out;非 wave 步骤恒返回 false(保守过门)。
@@ -248,6 +310,24 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
248
310
  const { runVerifyRequiredEvidenceCheck, printVerifyRequiredEvidenceCheck } = await import('../verify-postcheck.js')
249
311
  const evidenceCheck = runVerifyRequiredEvidenceCheck({ cwd, specBase, changeName })
250
312
  printVerifyRequiredEvidenceCheck(evidenceCheck)
313
+ // ── module-impact 死信探针(blocking,债单 D-1/D-5):更新结果表 pending/待办行 → 阻断 verify ──
314
+ // 与 archive 移动前校验(extractPendingDocSyncRows)同一口径,把死信号从 archive 提前到 verify:
315
+ // agent 在 verify 阶段就须完成文档同步并回填 done/skipped,而非拖到归档被拦(修复 perf-remediation
316
+ // 类「verify PASS → archive 才发现 pending」的时序漏洞)。
317
+ const verifyChangeDir = resolveChangeDir(cwd, progress, platformOpts?.specRoot)
318
+ if (verifyChangeDir) {
319
+ const impactPath = join(verifyChangeDir, 'module-impact.md')
320
+ if (existsSync(impactPath)) {
321
+ const { extractPendingDocSyncRows } = await import('./complete-handlers.js')
322
+ const pendingRows = extractPendingDocSyncRows(readFileSync(impactPath, 'utf8'))
323
+ if (pendingRows.length > 0) {
324
+ console.error(`\n❌ verify 阶段被阻断:module-impact.md「更新结果」表存在 ${pendingRows.length} 个未清 pending/待办项(死信)`)
325
+ for (const row of pendingRows) console.error(` - ${row}`)
326
+ console.error(' 文档同步是 verify 的收尾义务:请完成模块文档同步并回填状态为 done/skipped(说明原因),再重新完成 verify。')
327
+ return rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
328
+ }
329
+ }
330
+ }
251
331
  console.log('\n✅ 验证通过,下一步:sillyspec run archive')
252
332
  }
253
333
 
@@ -361,7 +441,11 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
361
441
  if (!executeRunId) {
362
442
  executeRunId = generateExecuteRunId()
363
443
  // 落盘(marker 缺失时 fallback 生成后写盘,保证后续 checkbox/gate 读到同一 ID)
364
- try { mkdirSync(runtimeRoot, { recursive: true }); writeFileSync(runIdFile, executeRunId + '\n') } catch {}
444
+ // D-001#1 fallback 写入点:mkdir execute-runs/<runId>/tasks 先于 marker(不变量:
445
+ // marker 在则目录在)。不 try/catch——异常直穿外层 catch 走 fail-closed 阻断
446
+ //(gate 自身写 run 目录失败不能静默放行完成)。
447
+ mkdirSync(join(runtimeRoot, 'execute-runs', executeRunId, 'tasks'), { recursive: true })
448
+ writeFileSync(runIdFile, executeRunId + '\n')
365
449
  }
366
450
  }
367
451
 
package/src/run/prompt.js CHANGED
@@ -390,6 +390,24 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
390
390
  promptText = applyRootPlaceholders(promptText, { specRoot: specSillyspec, docsRoot, projectsRoot, workflowsRoot, knowledgeRoot })
391
391
  }
392
392
 
393
+ // scan 漂移事实注入(债单 D-7 方案 A):brainstorm「加载项目上下文」读 scan 文档前,
394
+ // CLI 算出 staleness(source_commit vs HEAD)注入一行事实——"算事实注入"原则(债单第六节),
395
+ // 不是劝说指令。computeScanStaleness 内部全降级(无文档 null / git 失败 unknown),不阻断。
396
+ if (stageName === 'brainstorm' && promptText.includes('{SCAN_STALENESS}')) {
397
+ try {
398
+ const { computeScanStaleness } = await import('../scan-staleness.js')
399
+ const stalenessSpecBase = resolvePromptSpecBase(platformOpts, cwd)
400
+ const projectName = dbProjectName || basename(cwd)
401
+ const fact = computeScanStaleness({ projectRoot: cwd, specBase: stalenessSpecBase, projectName })
402
+ const injected = fact
403
+ ? `[docs-debt] scan 基线漂移:${fact.message}`
404
+ : '[docs-debt] scan 基线漂移:无 scan 文档(绿地项目),跳过判定'
405
+ promptText = promptText.replace(/\{SCAN_STALENESS\}/g, injected)
406
+ } catch (e) {
407
+ promptText = promptText.replace(/\{SCAN_STALENESS\}/g, `[docs-debt] scan 漂移检测异常(${e.message}),跳过判定`)
408
+ }
409
+ }
410
+
393
411
  // Knowledge hit report: execute 阶段注入匹配结果
394
412
  if (stageName === 'execute' && promptText.includes('{KNOWLEDGE_HIT_REPORT}')) {
395
413
  try {
@@ -420,6 +438,61 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
420
438
  }
421
439
  }
422
440
 
441
+ // docs-debt 模块文档欠账事实注入(2026-08-15 docs-debt-inject D-006,债单第六节"CLI 算事实"):
442
+ // Wave prompt {DOCS_DEBT} → computeDocsDebt(git diff × module-map)facts;空 facts 替换为空串
443
+ // (无债零输出,无残留占位符)。changedFiles 口径(D-001/FR-007):worktree 根
444
+ // (meta.json 所在)git status --porcelain + diff baselineCommit..HEAD 并集;无 worktree
445
+ // (in-place/非 execute worktree 场景)退 cwd。全降级不抛(异常 → 单行说明)。
446
+ if (stageName === 'execute' && promptText.includes('{DOCS_DEBT}')) {
447
+ try {
448
+ const { computeDocsDebt } = await import('../docs-debt.js')
449
+ const debtSpecBase = resolvePromptSpecBase(platformOpts, cwd)
450
+ const debtProjectName = dbProjectName || basename(cwd)
451
+ // worktree 根解析:changeName → <specBase>/.runtime/worktrees/<changeName>/ 存在即锚
452
+ let debtProjectRoot = cwd
453
+ const wtRoot = join(debtSpecBase, '.runtime', 'worktrees', changeName || '')
454
+ if (changeName && existsSync(wtRoot)) {
455
+ debtProjectRoot = wtRoot
456
+ }
457
+ // changedFiles:已提交(baseline..HEAD)∪ 未提交(status porcelain,非未跟踪)
458
+ const changed = new Set()
459
+ const { safeGit } = await import('../git-helper.js')
460
+ const statusRes = safeGit(debtProjectRoot, ['status', '--porcelain'], { trim: false })
461
+ if (!statusRes.error && statusRes.value) {
462
+ for (const line of statusRes.value.split('\n')) {
463
+ if (!line.trim()) continue
464
+ // "XY path" / rename "R old -> new" 取 new
465
+ const pathPart = line.slice(3).split('->').pop().trim().replace(/"/g, '')
466
+ if (pathPart && !pathPart.startsWith('.sillyspec/')) changed.add(pathPart.replace(/\\/g, '/'))
467
+ }
468
+ }
469
+ const { readFileSync: rf } = await import('node:fs')
470
+ const metaPath = join(wtRoot, 'meta.json')
471
+ if (changeName && existsSync(metaPath)) {
472
+ try {
473
+ const meta = JSON.parse(rf(metaPath, 'utf8'))
474
+ if (meta.baselineCommit) {
475
+ const diffRes = safeGit(debtProjectRoot, ['diff', '--name-only', `${meta.baselineCommit}..HEAD`], { trim: false })
476
+ if (!diffRes.error && diffRes.value) {
477
+ for (const f of diffRes.value.split('\n')) {
478
+ if (f.trim() && !f.trim().startsWith('.sillyspec/')) changed.add(f.trim().replace(/\\/g, '/'))
479
+ }
480
+ }
481
+ }
482
+ } catch { /* meta 读失败只用未提交集 */ }
483
+ }
484
+ const result = computeDocsDebt({
485
+ projectRoot: debtProjectRoot,
486
+ specBase: debtSpecBase,
487
+ projectName: debtProjectName,
488
+ changedFiles: [...changed],
489
+ })
490
+ promptText = promptText.replace(/\{DOCS_DEBT\}/g, result.facts)
491
+ } catch (e) {
492
+ promptText = promptText.replace(/\{DOCS_DEBT\}/g, `[docs-debt] 欠账计算异常(${e.message}),跳过`)
493
+ }
494
+ }
495
+
423
496
  // Execute: 注入 currentExecuteRunId(从变更专属标记文件读取)
424
497
  if (stageName === 'execute' && promptText.includes('{EXECUTE_RUN_ID}')) {
425
498
  let runId = ''
@@ -442,7 +515,15 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
442
515
  const { generateExecuteRunId } = await import('../task-review.js')
443
516
  runId = generateExecuteRunId()
444
517
  // 落盘(与启动站点一致),保证 agent 收到的 ID == gate/checkbox 读取的 ID
445
- try { mkdirSync(runtimeRoot, { recursive: true }); writeFileSync(runIdFile, runId + '\n') } catch {}
518
+ // D-001#1 fallback 写入点:mkdir execute-runs/<runId>/tasks 先于 marker(不变量:marker 在则目录在)。
519
+ // 渲染路径异常不能炸 prompt 输出 → catch 内 console.error 留痕 + 保留降级(ID 注入继续,
520
+ // 目录由下游 review 写入 / 其他写入点补齐)。
521
+ try {
522
+ mkdirSync(join(runtimeRoot, 'execute-runs', runId, 'tasks'), { recursive: true })
523
+ writeFileSync(runIdFile, runId + '\n')
524
+ } catch (e) {
525
+ console.error(`[sillyspec] execute run marker/目录写入失败(降级继续,ID ${runId} 仍注入 prompt): ${e.message}`)
526
+ }
446
527
  }
447
528
  promptText = promptText.replace(/\{EXECUTE_RUN_ID\}/g, runId)
448
529
  }
@@ -50,6 +50,29 @@ export function printQuickAuditReview(review) {
50
50
  console.log(`\n✅ quick 变更边界审计 — SAFE (本轮新增变更 ${review.changedFiles.length} 个文件)`)
51
51
  }
52
52
  }
53
+ // D-8 文档欠账显性化(advisory,独立于 status 三态都打):改了源码没动文档 → 一行欠账标记。
54
+ // 不阻断、不解锁、纯显性化——累积欠账可事后审计 QUICKLOG reasons 追溯。
55
+ if (review.docSyncHint && review.docSyncHint.touchedSource > 0 && review.docSyncHint.docFiles.length === 0) {
56
+ console.warn(`\n📝 文档欠账标记(D-8):本次 ${review.docSyncHint.touchedSource} 个源码文件改动未同步任何模块文档。`)
57
+ // O-1(docs-signals-o12):模块归属——从"改了 N 文件"到"欠在哪"(matchFilesToModules 纯函数,map 缺失时空数组降级)
58
+ if (Array.isArray(review.docSyncHint.modules) && review.docSyncHint.modules.length > 0) {
59
+ console.warn(` 涉及模块:${review.docSyncHint.modules.map(m => m.id).join(' · ')}(模块卡待同步,execute 场景详见 [docs-debt] 块)`)
60
+ }
61
+ console.warn(` quick 不强制文档同步,但欠账已记录(QUICKLOG reasons)。若改动触及接口/契约,建议顺手同步模块文档。`)
62
+ }
63
+ if (review.docsCheckHint && review.docsCheckHint.invalid > 0) {
64
+ console.warn(`\n📎 文档引用失效(docs check):本次改动的文档含 ${review.docsCheckHint.invalid}/${review.docsCheckHint.total} 处失效 file:line 引用。`)
65
+ console.warn(` 行号漂移 → 更新到当前源码;文件删改名 → 更新引用路径。跑 sillyspec docs check 可看完整清单。`)
66
+ console.warn(` 引用格式:\`src/foo.js:42\`(或 42-48)+ 同行反引号代码符号(如 \`runDocsCheck\`)——符号可让 --suggest 给出候选行号。`)
67
+ }
68
+ // task-03: 活文档引用漂移(advisory 不阻断,不改 status 三态判定)——方向与上方互补:
69
+ // 上方查「本次改的文档」,这里查「本次改的 src 被活文档(platform-interface-map 等)引用」。
70
+ if (review.docsCheckHint && review.docsCheckHint.livingDocDrift) {
71
+ const drift = review.docsCheckHint.livingDocDrift
72
+ const total = typeof drift.total === 'number' ? drift.total : drift.files.length
73
+ console.warn(`\n📎 活文档引用漂移:改动 ${drift.files.length}/${total} 被 ${drift.docs.join('、')} 引用——建议顺手跑 docs check 修引用行号。`)
74
+ console.warn(` 文件:${drift.files.join(', ')}`)
75
+ }
53
76
  }
54
77
 
55
78
  /**