sillyspec 3.28.2 → 3.28.3

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 (41) hide show
  1. package/.claude/skills/sillyspec-auto/SKILL.md +54 -93
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -114
  3. package/.claude/skills/sillyspec-continue/SKILL.md +17 -47
  4. package/.claude/skills/sillyspec-execute/SKILL.md +222 -214
  5. package/.claude/skills/sillyspec-knowledge/SKILL.md +40 -270
  6. package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
  7. package/.claude/skills/sillyspec-propose/SKILL.md +16 -87
  8. package/.claude/skills/sillyspec-resume/SKILL.md +21 -70
  9. package/.claude/skills/sillyspec-state/SKILL.md +25 -64
  10. package/.claude/skills/sillyspec-workspace/SKILL.md +33 -175
  11. package/package.json +1 -1
  12. package/src/archive-delta.js +45 -2
  13. package/src/constants.js +6 -0
  14. package/src/design-facts.js +64 -0
  15. package/src/dispatch/backends/sillyhub-mcp.js +17 -5
  16. package/src/docs-check.js +119 -27
  17. package/src/docs-migrate.js +124 -0
  18. package/src/index.js +172 -22
  19. package/src/progress/change-registry.js +19 -0
  20. package/src/progress.js +2 -0
  21. package/src/run/command.js +129 -4
  22. package/src/run/complete-handlers.js +10 -3
  23. package/src/run/complete.js +59 -5
  24. package/src/run/gates.js +22 -2
  25. package/src/run/progress-confirm.js +66 -0
  26. package/src/run/prompt.js +139 -31
  27. package/src/run/quick-audit.js +12 -5
  28. package/src/run/scan-profile.js +20 -2
  29. package/src/run/shared.js +10 -5
  30. package/src/run/stage.js +7 -0
  31. package/src/scan-postcheck.js +5 -1
  32. package/src/stages/brainstorm-auto.js +2 -1
  33. package/src/stages/brainstorm.js +9 -17
  34. package/src/stages/execute.js +10 -14
  35. package/src/stages/plan.js +2 -1
  36. package/src/stages/quick.js +4 -2
  37. package/src/stages/verify.js +13 -15
  38. package/src/task-review.js +200 -0
  39. package/src/verify-postcheck.js +164 -30
  40. package/templates/agents-instruction.md +1 -1
  41. package/.claude/skills/verify-per-user/SKILL.md +0 -6
@@ -21,7 +21,25 @@ import { existsSync, readFileSync, mkdirSync, writeFileSync, appendFileSync } fr
21
21
  import { writeAtomicSync } from '../fs-atomic.js'
22
22
  import { withFileLock } from '../quicklog.js'
23
23
  import { triggerSync, WAIT_MARKER_RE, getStageSteps, formatWaitOptions, resolveRuntimeRoot, getOrCreateMultiRepoContext, resolveChangeDir } from './shared.js'
24
+
25
+ // auto 模式 brainstorm 步骤表感知解析(2026-09-08 E2E 实证 bug:--done/--wait 推进主模式 8 步表,
26
+ // 与 runAutoMode getAutoSteps 的 4 步 auto 表双轨互踩——ensureAutoStage 判非 auto 表重种清零,
27
+ // 进度永远回 step1)。DB 现存 brainstorm 表为 auto 形态(4 步+首名「进度确认与上下文加载」)时,
28
+ // complete 路径与渲染路径同源用 brainstormAutoDef.steps。
29
+ async function getStageStepsAutoAware(stageName, cwd, progress, specDir) {
30
+ const steps = await getStageSteps(stageName, cwd, progress, specDir)
31
+ if (stageName !== 'brainstorm') return steps
32
+ const existing = progress?.stages?.brainstorm?.steps
33
+ const isAutoTable = Array.isArray(existing) && existing.length === 4
34
+ && existing[0]?.name === '进度确认与上下文加载'
35
+ if (!isAutoTable) return steps
36
+ const { definition: brainstormAutoDef } = await import('../stages/brainstorm-auto.js')
37
+ return brainstormAutoDef.steps
38
+ }
39
+
24
40
  import { executeScanPreflight, executeScanPostcheck, computeScanProfile, executeScanDetectProjects, executeScanResumeCheck, executeScanFinalize } from './scan-profile.js'
41
+ import { executeProgressConfirm } from './progress-confirm.js'
42
+ import { AUXILIARY_STAGES } from '../constants.js'
25
43
  import { executePlanPostcheck as runPlanPostcheckLib } from '../stages/plan-postcheck.js'
26
44
  import { outputStep, collectStageWaitHistory } from './prompt.js'
27
45
  import { enforceDepsGate, enforceReviewJsonGate, enforceSymbolImpactGate, warnMissingUiPrototype, completeStageGates, readDesignScale } from './gates.js'
@@ -220,7 +238,7 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
220
238
  }
221
239
 
222
240
  // ── requiresWait 硬门控 ──
223
- const defStepsForCurrent = await getStageSteps(stageName, cwd, progress, platformOpts?.specRoot || null)
241
+ const defStepsForCurrent = await getStageStepsAutoAware(stageName, cwd, progress, platformOpts?.specRoot || null)
224
242
  // def↔DB 一致性守卫(坑 execute-step-table-drift,2026-08-20 实证):runCommand 入口已按名
225
243
  // 重播种(ensureStageSteps),但同一命令进程内 plan.md 若再次被改(或绕过 runCommand 直调
226
244
  // completeStep 的路径),def 与 DB 步数错位会让 currentIdx 的门控/prompt 施加到错误的步骤上
@@ -312,6 +330,29 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
312
330
  }
313
331
  }
314
332
 
333
+ // ── design.md 文件清单行级核验(2026-09-07-ir-hardening D-004@v1,FR-02)──
334
+ // 幻觉路径 ERROR(design_file_ref_invalid,与 plan 侧 validateTargetFiles 同语义先例);
335
+ // NEW: 前缀豁免 / glob 跳过 / 清单缺失 WARNING。挂点与决策模块域核验同点位(brainstorm 末步)。
336
+ if (stageName === 'brainstorm' && steps[currentIdx]?.name?.includes('生成规范文件') && changeName) {
337
+ try {
338
+ const { validateDesignFileList } = await import('../design-facts.js')
339
+ const _dflResult = validateDesignFileList({
340
+ changeDir: join(specBase, 'changes', changeName),
341
+ cwd,
342
+ })
343
+ if ((_dflResult.errors || []).length > 0) {
344
+ console.error(`❌ ── design 文件清单核验阻断(本次 --done 未完成,进度未推进)──`)
345
+ for (const _dflErr of _dflResult.errors) console.error(` • ${_dflErr.message}`)
346
+ console.error(` 修复:更正 design.md「文件变更清单」对应行路径,或计划新建文件加 NEW: 前缀后重跑:`)
347
+ console.error(` sillyspec run brainstorm --done${changeName ? ` --change ${changeName}` : ''} --output "修复说明"`)
348
+ process.exit(1)
349
+ }
350
+ for (const _dflWarn of (_dflResult.warnings || [])) console.warn(` ⚠️ ${_dflWarn}`)
351
+ } catch (_dflEx) {
352
+ console.warn(` ⚠️ design 清单核验自身异常,fail-open 放行(不误拦本次完成):${_dflEx && _dflEx.message ? _dflEx.message : _dflEx}`)
353
+ }
354
+ }
355
+
315
356
  // ── noAI 步骤硬门(坑 noai-done-bypass):noAI 步骤的确定性校验不可被 --done 绕过 ──
316
357
  // 正常路径 agent 跑 `run <stage>` 推进到 noAI step 时,runStage 自动执行 _cliAction
317
358
  // (stage.js noAI 分支,不写 step output);若 agent 对 noAI step 直接 --done,此前
@@ -336,6 +377,9 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
336
377
  await executeScanFinalize(cwd, platformOpts)
337
378
  } else if (_cliAction === 'planPostcheck') {
338
379
  await runPlanPostcheckLib({ cwd, specRoot: platformOpts?.specRoot, resolveChangeDir, progress })
380
+ } else if (_cliAction === 'progressConfirm') {
381
+ // 与 stage.js noAI 分支同语义(brainstorm/execute/verify step1 进度确认)
382
+ executeProgressConfirm({ stageName, cwd, stageData, changeName, pm })
339
383
  } else {
340
384
  throw new Error(`noAI 步骤 ${steps[currentIdx].name} 的未知 _cliAction: ${_cliAction}——请在 complete.js 注册对应分支`)
341
385
  }
@@ -483,6 +527,11 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
483
527
  }
484
528
  }
485
529
 
530
+ // 主阶段完成钉 currentStage(troubleshooting #56 根因修复,2026-09-08):
531
+ // `run <stage> --done` 直达完成不经过裸 run 入口(唯一写点 stage.js:227),
532
+ // currentStage 停留旧阶段 → 下游 checkTransition 用 stale prevStage 误拦
533
+ // (实证:verify 全 --done 完成后归档报 execute→archive)。与入口写点同语义:辅助阶段不写。
534
+ if (!AUXILIARY_STAGES.includes(stageName)) progress.currentStage = stageName
486
535
  stageData.status = 'completed'
487
536
  stageData.completedAt = new Date().toLocaleString('zh-CN',{hour12:false})
488
537
  progress.lastActive = new Date().toLocaleString('zh-CN',{hour12:false})
@@ -606,7 +655,7 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
606
655
  }
607
656
  }
608
657
 
609
- const defSteps = await getStageSteps(stageName, cwd, progress, platformOpts?.specRoot || null)
658
+ const defSteps = await getStageStepsAutoAware(stageName, cwd, progress, platformOpts?.specRoot || null)
610
659
  console.log(`✅ Step ${currentIdx + 1}/${steps.length} 完成:${steps[currentIdx].name}\n`)
611
660
 
612
661
  // Workflow post_check(W6 Step6 抽至 complete-handlers.js handleWorkflowPostCheck)
@@ -1007,7 +1056,7 @@ export async function waitStep(pm, progress, stageName, cwd, outputText, waitRea
1007
1056
 
1008
1057
  // maxWaitRounds 硬上限:达到后拒绝继续 --wait
1009
1058
  const currentStep = stageData.steps[currentIdx]
1010
- const defSteps = await getStageSteps(stageName, cwd, progress, platformOpts?.specRoot || null)
1059
+ const defSteps = await getStageStepsAutoAware(stageName, cwd, progress, platformOpts?.specRoot || null)
1011
1060
  const stepDef = defSteps?.[currentIdx] || {}
1012
1061
  const maxWaitRounds = currentStep.maxWaitRounds ?? stepDef.maxWaitRounds
1013
1062
  const currentWaitRound = currentStep.waitRound || 0
@@ -1121,7 +1170,7 @@ export async function continueStep(pm, progress, stageName, cwd, answer, options
1121
1170
  } else {
1122
1171
  currentIdx = waitingSteps[0].idx
1123
1172
  }
1124
- const defSteps = await getStageSteps(stageName, cwd, progress, platformOpts?.specRoot || null)
1173
+ const defSteps = await getStageStepsAutoAware(stageName, cwd, progress, platformOpts?.specRoot || null)
1125
1174
  const currentStepDef = defSteps?.[currentIdx] || {}
1126
1175
  const currentStep = stageData.steps[currentIdx]
1127
1176
  const isRepeatableWait = currentStepDef.repeatableWait === true || currentStep.repeatableWait === true
@@ -1219,6 +1268,11 @@ export async function continueStep(pm, progress, stageName, cwd, answer, options
1219
1268
  if (nextPendingIdx === -1 && nextWaitingIdx === -1) {
1220
1269
  stageData.status = 'completed'
1221
1270
  stageData.completedAt = now
1271
+ // 主阶段完成钉 currentStage(troubleshooting #56 根因修复,2026-09-08):
1272
+ // `run <stage> --done` 直达完成不经过裸 run 入口(唯一写点 stage.js:227),
1273
+ // currentStage 停留旧阶段 → 下游 checkTransition 用 stale prevStage 误拦
1274
+ // (实证:verify 全 --done 完成后归档报 execute→archive)。与入口写点同语义:辅助阶段不写。
1275
+ if (!AUXILIARY_STAGES.includes(stageName)) progress.currentStage = stageName
1222
1276
  // persist _write 移到 completeStageGates 成功之后(task-03 / review-2026-08-09 #2):gate 异常/失败 → rollback 回 in-progress 落盘,此处未到 _write,DB 不留假 completed。
1223
1277
  // 阶段完成收尾共享管线(含 execute worktree cleanup),消除 continueStep 完成分支绕过 gate 的 S2(task-01 抽出)。
1224
1278
  // gate 失败已 rollback,early-return 跳过下方"阶段已完成/下一步"提示(与 completeStep 同语义)。
@@ -1303,7 +1357,7 @@ export async function skipStep(pm, progress, stageName, cwd, changeName, platfor
1303
1357
  process.exit(1)
1304
1358
  }
1305
1359
 
1306
- const defSteps = await getStageSteps(stageName, cwd, progress, platformOpts?.specRoot || null)
1360
+ const defSteps = await getStageStepsAutoAware(stageName, cwd, progress, platformOpts?.specRoot || null)
1307
1361
  const stepDef = defSteps ? defSteps[currentIdx] : null
1308
1362
  if (stepDef && !stepDef.optional) {
1309
1363
  console.error(`❌ 步骤 "${steps[currentIdx].name}" 不可跳过`)
package/src/run/gates.js CHANGED
@@ -674,13 +674,15 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
674
674
  // 声明=scope creep)启发式噪音面大(并行 WIP 剔除后仍可能有工具产物),WARNING 放行留审计;
675
675
  // skipped/degraded(无 task 卡 / 全无声明 / git 不可用)降级不误红(存量变更零红门禁)。
676
676
  // 阻断形态照 runVerifyTestCheck 先例:逐条列 task + path + 修复提示 + rollbackCompletionAndReturn。
677
- const { reconcileTargetFiles } = await import('../verify-postcheck.js')
677
+ const { reconcileTargetFiles, isStrictChange } = await import('../verify-postcheck.js')
678
678
  const reconcileRuntimeRoot = resolveRuntimeRoot(platformOpts, specBase)
679
679
  const reconcileCheck = reconcileTargetFiles({
680
680
  cwd, specBase, changeName,
681
681
  // runtimeRoot 口径与上方 parity 对账同源(B3 apply-pathspec 兜底源在此根下;平台模式下
682
682
  // specBase 与 .runtime 分离,不传会读不到兜底清单 → 假降级)
683
683
  runtimeRoot: reconcileRuntimeRoot,
684
+ // IR 严格档(2026-09-07-ir-hardening D-003@v1):主仓卡全零声明 → strictViolation → ERROR
685
+ strictMode: isStrictChange({ pm, cwd, changeName }),
684
686
  })
685
687
  const reconcileEnvelope = buildReconcileTargetFilesEnvelope(reconcileCheck)
686
688
  const reconcileBlocked = printReconcileTargetFilesCheck(reconcileCheck, reconcileEnvelope)
@@ -705,6 +707,9 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
705
707
  // 的锚点产物实际都在 changeDir,此参为统一传参签名对齐预留,口径一处定义不二算
706
708
  cwd, specBase, changeName,
707
709
  runtimeRoot: reconcileRuntimeRoot,
710
+ // IR 严格档(2026-09-07-ir-hardening D-002@v1):created_at ≥ IR_STRICT_SINCE 的变更
711
+ // 探针子节全缺 = ERROR(存量 skip 语义零变化);fail-open(读不到 → false 落豁免)
712
+ strictMode: isStrictChange({ pm, cwd, changeName }),
708
713
  })
709
714
  const probeEnvelope = buildProbeConsistencyEnvelope(probeCheck)
710
715
  const probeBlocked = printProbeConsistencyCheck(probeCheck, probeEnvelope)
@@ -1357,6 +1362,14 @@ export function printReconcileTargetFilesCheck(r, envelope = buildReconcileTarge
1357
1362
  console.warn(` 对账口径 actual=${(r.sources || []).join(' + ') || 'n/a'}(③类误报先核对 sources 与并行会话剔除提示)。`)
1358
1363
  return false
1359
1364
  }
1365
+ // IR 严格档(2026-09-07-ir-hardening D-003):strictViolation → ERROR 阻断(与②类同处置)
1366
+ if (r.status === 'skipped' && r.strictViolation) {
1367
+ console.error(`
1368
+ ❌ target_files 对账阻断:${r.skipReason}`)
1369
+ console.error(` ${r.strictViolation.message}`)
1370
+ console.error(` 信封 code: ${r.strictViolation.code}`)
1371
+ return true
1372
+ }
1360
1373
  // 降级(skipped/degraded):WARNING 放行,skipReason 一句话(存量卡/git 不可用,不误红)
1361
1374
  if (r.status === 'skipped' || r.status === 'degraded') {
1362
1375
  console.warn(`\n⚠️ target_files 对账${r.status === 'degraded' ? '降级' : '跳过'}(不阻断):${r.skipReason}`)
@@ -1385,6 +1398,12 @@ export function printReconcileTargetFilesCheck(r, envelope = buildReconcileTarge
1385
1398
  export function buildProbeConsistencyEnvelope(r) {
1386
1399
  const blocked = r.status === 'mismatch' && r.severity === 'error'
1387
1400
  const drifted = r.status === 'mismatch' && !blocked
1401
+ // IR 严格档独立 code(2026-09-07-ir-hardening D-002):首条 mismatch 携带
1402
+ // probe_prefill_missing_strict 时路由专用 code(平台/上游按 code 分支可区分
1403
+ // 「篡改预填段」与「从未生成预填段」两类 ERROR)
1404
+ const strictMissing = blocked
1405
+ && Array.isArray(r.mismatches) && r.mismatches.length > 0
1406
+ && r.mismatches[0] && r.mismatches[0].code === 'probe_prefill_missing_strict'
1388
1407
  const supportedFixes = []
1389
1408
  if (blocked) {
1390
1409
  supportedFixes.push('如实重生成:跑 `sillyspec verify-probes --change <变更名> --init` 重新落盘机械预填段,再基于新预填段如实补写正文结论(删改预填段过门是 Step7 防篡改纪律红线)')
@@ -1396,7 +1415,8 @@ export function buildProbeConsistencyEnvelope(r) {
1396
1415
  }
1397
1416
  return {
1398
1417
  // code 机器路由键(task-02 JSDoc):四状态→四 code,供平台/上游按 code 分支
1399
- code: blocked ? 'probe_consistency_mismatch'
1418
+ code: strictMissing ? 'probe_prefill_missing_strict'
1419
+ : blocked ? 'probe_consistency_mismatch'
1400
1420
  : drifted ? 'probe_consistency_drift'
1401
1421
  : (r.status === 'ok' ? 'probe_consistency_ok' : 'probe_consistency_skipped'),
1402
1422
  name: 'probe_consistency',
@@ -0,0 +1,66 @@
1
+ /**
2
+ * progressConfirm — 主流程 Step1「进度确认」的 noAI 动作(brainstorm / execute / verify)
3
+ * (2026-09-07 noAI 主流程首批;机制先例:scan 系 noAI 步 + planPostcheck,见 run/stage.js noAI 分支)
4
+ *
5
+ * noAI 化依据(step1 职责在历次加固后已全部机械化):
6
+ * - 阶段路由:`run <stage>` 显式调用即写 currentStage(run/stage.js D-003@v1),
7
+ * 阶段顺序违规由 checkTransition 拦截——原 prompt 的「阶段不符则停止」检查已残留化
8
+ * ({PROGRESS_SNAPSHOT} 注入文案本就写着「CLI 已路由到本阶段」);
9
+ * - 进度摘要:快照三行(变更/阶段/步骤位置)由 CLI 注入,agent 的「输出当前进度摘要」
10
+ * 只是把注入内容复述一遍再 --done——整轮往返零判断增量;
11
+ * - brainstorm 变更名检查:自动名在创建时 CLI 已打印 rename 提示(run/command.js),
12
+ * 此处机械复检一次模式命中;命中只打印 ⚠️ 建议(noAI 步的 console 输出仍在 CLI 工具
13
+ * 结果里,agent 可见、可在后续步骤与用户交互改名)——不 throw、不阻塞推进。
14
+ * - execute 执行范围:$ARGUMENTS 是会话层信息,CLI 不掺入;本步只补 tasks 勾选计数
15
+ * (readPlanCheckboxStatus 既有机器事实),范围以用户在会话中给出的为准。
16
+ * - verify 测试提示:原 prompt 首段的「CLI 在 --done 统一跑测试」要点改为 console 提示。
17
+ *
18
+ * 输出纪律:只 console 打印(noAI 步不写 step output,与 scan 系动作一致);
19
+ * 纯确认步无失败语义,不 throw。
20
+ */
21
+ import { join } from 'node:path'
22
+
23
+ /** brainstorm 自动生成变更名的模式(run/command.js:1083 `${date}-new-change-${hex}`) */
24
+ const AUTO_CHANGE_NAME_RE = /^\d{4}-\d{2}-\d{2}-new-change-[a-f0-9]+$/
25
+
26
+ /**
27
+ * 执行「进度确认」noAI 动作:打印进度快照 + 阶段专属机械提示。
28
+ *
29
+ * @param {object} opts
30
+ * @param {string} opts.stageName - brainstorm / execute / verify
31
+ * @param {string} opts.cwd
32
+ * @param {object} opts.stageData - progress.stages[stageName](steps 数组)
33
+ * @param {string|null} opts.changeName
34
+ * @param {object|null} [opts.pm] - ProgressManager(execute 档 tasks 勾选计数用;缺省跳过该段)
35
+ */
36
+ export function executeProgressConfirm({ stageName, cwd, stageData, changeName, pm = null }) {
37
+ const steps = Array.isArray(stageData?.steps) ? stageData.steps : []
38
+ const done = steps.filter(s => s.status === 'completed').length
39
+ console.log('📊 进度快照(CLI 注入,无需跑 progress show):')
40
+ console.log(` - 变更:${changeName || '(未知)'}`)
41
+ console.log(` - 阶段:${stageName}(CLI 已路由到本阶段)`)
42
+ console.log(` - 步骤:${done}/${steps.length} 已完成;本步已由 CLI 自动确认`)
43
+
44
+ if (stageName === 'brainstorm' && changeName && AUTO_CHANGE_NAME_RE.test(changeName)) {
45
+ console.log('')
46
+ console.log(`⚠️ 变更名「${changeName}」疑似自动生成——建议与用户确认语义化名称后执行:`)
47
+ console.log(` sillyspec change-rename ${changeName} <新名称>(勿用 mv,会漏改进度库引用)`)
48
+ }
49
+
50
+ if (stageName === 'execute' && pm && changeName) {
51
+ try {
52
+ const changeDir = join(pm._getSpecDir(cwd), 'changes', changeName)
53
+ const plan = pm.readPlanCheckboxStatus(changeDir)
54
+ if (plan && plan.total > 0) {
55
+ console.log(` - 任务勾选:tasks ${plan.checked}/${plan.total}(执行范围以用户会话指定为准,未指定则全部 Wave)`)
56
+ }
57
+ } catch { /* 勾选计数 fail-soft:plan.md 缺失/解析失败不阻塞确认步 */ }
58
+ }
59
+
60
+ if (stageName === 'verify') {
61
+ console.log('')
62
+ console.log('💡 `run verify` 只下发指令不跑测试——全量测试由 CLI 在最后 --done 统一执行')
63
+ console.log(' (local.yaml 的 commands.test,同步对账可能耗时较长);后续「运行测试和质量扫描」')
64
+ console.log(' 步骤无需手动跑全量,只做 lint/静态检查 + 可选快速冒烟。')
65
+ }
66
+ }
package/src/run/prompt.js CHANGED
@@ -185,7 +185,20 @@ export function collectStageWaitHistory(progress, stageName) {
185
185
  /**
186
186
  * 输出当前步骤的 prompt
187
187
  */
188
- export async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjectName, platformOpts = {}, prevStepAnswer = null, waitHistory = null) {
188
+ /**
189
+ * requiresUser —— auto driver 元数据(change: 2026-09-08-auto-driver,D-002@v1)的四源判定纯函数:
190
+ * step.requiresWait / conditionalWait / requiresConfirm 三键(step 定义权威信号)+ WAIT_MARKER_RE 正文扫描
191
+ * (execute/verify 的 step 定义无三键,纯三源会漏真 wait 步)。全缺兜底 **false**——auto 默认语义是
192
+ * 继续驱动(Grill P1-①:保守 true 会让 execute/verify 逐步人工确认,推翻 FR-01)。
193
+ * 与 outputStep 内既有 mayNeedWait 计算同源(:1019 附近),仅多 requiresConfirm 一源。
194
+ */
195
+ export function requiresUser(step, promptText) {
196
+ if (!step) return false
197
+ if (step.requiresWait === true || step.conditionalWait === true || step.requiresConfirm === true) return true
198
+ return WAIT_MARKER_RE.test(String(promptText || ''))
199
+ }
200
+
201
+ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjectName, platformOpts = {}, prevStepAnswer = null, waitHistory = null, autoMeta = null) {
189
202
  const step = steps[stepIndex]
190
203
  const total = steps.length
191
204
  // ── 越界防御 ──
@@ -199,6 +212,16 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
199
212
  console.error(` 请检查 --change 指定的变更目录是否存在、specRoot 是否正确,然后重试。`)
200
213
  return false
201
214
  }
215
+
216
+ // ── 首个 agent 可见步索引(「首步一次建立」类注入的锚点)──
217
+ // step0 为 noAI(brainstorm/execute/verify 的 step1「进度确认」,2026-09-07)时不渲染 prompt,
218
+ // persona/护栏/完成契约/铁律/平台路径约束等 step0 专属注入须顺延到首个渲染步,否则整段丢失
219
+ //(spec-dir.test.mjs Test4 平台 specDir 断言即此坑的行为探针)。全 noAI 防御性回退 0。
220
+ const firstRenderableIdx = (() => {
221
+ const i = steps.findIndex(s => s?.noAI !== true)
222
+ return i === -1 ? 0 : i
223
+ })()
224
+
202
225
  const projectName = dbProjectName || basename(cwd)
203
226
 
204
227
  // ── Revision context injection ──
@@ -244,8 +267,8 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
244
267
  console.log(`changeDir: ${changeDir}`)
245
268
  }
246
269
  console.log(`---\n`)
247
- // persona 只在 stage 首步注入(step0)——角色设定一次即可,后续 step 重复注入纯属 token 浪费
248
- if (personas[stageName] && stepIndex === 0) {
270
+ // persona 只在首个 agent 可见步注入——角色设定一次即可,后续 step 重复注入纯属 token 浪费
271
+ if (personas[stageName] && stepIndex === firstRenderableIdx) {
249
272
  console.log(personas[stageName])
250
273
  console.log('')
251
274
  }
@@ -255,7 +278,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
255
278
 
256
279
  console.log(`## Step ${stepIndex + 1}/${total}: ${step.name}\n`)
257
280
  if (guardrails) {
258
- if (stepIndex === 0) {
281
+ if (stepIndex === firstRenderableIdx) {
259
282
  console.log(guardrails.trim())
260
283
  console.log('')
261
284
  } else {
@@ -398,13 +421,15 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
398
421
  platformDirectives.push(`workspace_id: ${platformOpts.workspaceId}`)
399
422
  }
400
423
  // 平台 directives 前置条件:
401
- // - step0(对齐 persona 仅 step0 策略):路径列表/Write 规则/占位符映射一次建立即可;
424
+ // - 首个 agent 可见步(对齐 persona 仅 step0 策略):路径列表/Write 规则/占位符映射
425
+ // 一次建立即可——step0 为 noAI(brainstorm/execute/verify 的 step1「进度确认」,
426
+ // 2026-09-07)时首个可见步顺延,按 defSteps 计算(坑同 scan quick 档 noAI preflight);
402
427
  // - scan 阶段所有 step:scan 的 profileDirectives 本就每步注入(见下方 L917),平台路径
403
428
  // 约束也需每步可见——否则 quick profile 的 step0 是 noAI preflight(自动执行),run scan
404
429
  // 输出的首个可见 step 是 step1,会漏掉 specDir 路径,导致平台模式写入约束丢失。
405
430
  // 后续 step 靠 changeDir header + footer 平台规则(L1077+)维持,platformDirectives
406
431
  // 每步重复约 500 tokens 仅在 scan(步数有限)可接受。
407
- if (stepIndex === 0 || stageName === 'scan') {
432
+ if (stepIndex === firstRenderableIdx || stageName === 'scan') {
408
433
  promptText = platformDirectives.join('\n') + '\n\n' + promptText
409
434
  }
410
435
  } else {
@@ -506,6 +531,80 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
506
531
  promptText = promptText.replace(/\{SCAN_FACTS\}/g, factsInjected)
507
532
  }
508
533
 
534
+ // ── 机械事实注入三件(2026-09-07 注入缺口批次):{LOCAL_COMMANDS} / {GIT_DIRTY} / {TASKS_CHECKBOX} ──
535
+ // CLI 渲染 prompt 时已能确定性拿到的机械事实,替代 agent 各自跑一趟 cat local.yaml /
536
+ // git status / 手数勾选(全流程 7+ 处步骤的高频机械读)。全部 fail-soft:异常注入单行
537
+ // 说明,不留残留占位符;替换后占位符即消失(不存在的档位自然零输出)。
538
+
539
+ // ① {LOCAL_COMMANDS}:local.yaml commands 段原文(brainstorm/plan/execute/verify/quick 多步消费)
540
+ if (promptText.includes('{LOCAL_COMMANDS}')) {
541
+ let localBlock = ''
542
+ try {
543
+ const lcSpecBase = resolvePromptSpecBase(platformOpts, cwd)
544
+ const localPath = join(lcSpecBase, 'local.yaml')
545
+ if (existsSync(localPath)) {
546
+ const lcLines = String(readFileSync(localPath, 'utf8')).replace(/\r\n?/g, '\n').split('\n')
547
+ const start = lcLines.findIndex(l => /^commands:\s*(#.*)?$/.test(l))
548
+ if (start !== -1) {
549
+ const body = []
550
+ for (let i = start + 1; i < lcLines.length && !/^\S/.test(lcLines[i]); i++) body.push(lcLines[i])
551
+ // unavailable 标记的条目按未配置语义剔除行(与 extractTestCommand 同口径)
552
+ const avail = body.filter(l => !/^\s*[\w.-]+:\s*['"]?unavailable['"]?\s*(#.*)?$/i.test(l))
553
+ const trimmed = avail.join('\n').trim()
554
+ localBlock = trimmed
555
+ ? 'commands:\n' + trimmed
556
+ : '(local.yaml 存在但 commands 段为空——可跑 `sillyspec local detect` 补默认骨架)'
557
+ } else {
558
+ localBlock = '(local.yaml 存在但无 commands 段——可跑 `sillyspec local detect` 补默认骨架)'
559
+ }
560
+ } else {
561
+ localBlock = '(未配置 local.yaml——先跑 `sillyspec local detect` 生成骨架,以命令输出为准)'
562
+ }
563
+ } catch (e) {
564
+ localBlock = `(构建命令注入失败:${e.message}——可自行读 local.yaml)`
565
+ }
566
+ promptText = promptText.replace(/\{LOCAL_COMMANDS\}/g, localBlock)
567
+ }
568
+
569
+ // ② {GIT_DIRTY}:工作区脏文件清单(git status --porcelain,quick 收尾步消费)
570
+ if (promptText.includes('{GIT_DIRTY}')) {
571
+ let dirtyBlock = ''
572
+ try {
573
+ const { gitQuiet } = await import('../git-helper.js')
574
+ // gitQuiet 返回裸 string(失败 null)——非 safeGit 的 {value,error} 形状
575
+ const lines = String(gitQuiet(cwd, ['status', '--porcelain']) || '').split('\n').filter(Boolean)
576
+ dirtyBlock = lines.length === 0
577
+ ? '(工作区干净,无脏文件)'
578
+ : lines.slice(0, 40).join('\n') + (lines.length > 40 ? `\n(…共 ${lines.length} 行,超出 40 行截断——完整清单可自行跑 git status --porcelain)` : '')
579
+ } catch (e) {
580
+ dirtyBlock = `(脏文件注入失败:${e.message}——可自行跑 git status --porcelain)`
581
+ }
582
+ promptText = promptText.replace(/\{GIT_DIRTY\}/g, dirtyBlock)
583
+ }
584
+
585
+ // ③ {TASKS_CHECKBOX}:tasks.md 任务勾选状态投影(verify 逐项检查步消费——省 agent 手数)
586
+ if (promptText.includes('{TASKS_CHECKBOX}')) {
587
+ let tasksBlock = ''
588
+ try {
589
+ const tcSpecBase = resolvePromptSpecBase(platformOpts, cwd)
590
+ const tasksPath = changeName ? join(tcSpecBase, 'changes', changeName, 'tasks.md') : null
591
+ if (tasksPath && existsSync(tasksPath)) {
592
+ const re = /^(\s*[-*]\s+\[([ xX])\]\s+task-\d+.*)$/
593
+ const rows = String(readFileSync(tasksPath, 'utf8')).replace(/\r\n?/g, '\n').split('\n')
594
+ .map(l => l.match(re)).filter(Boolean).map(m => m[1].trim())
595
+ const checked = rows.filter(r => /\[x\]/i.test(r)).length
596
+ tasksBlock = rows.length === 0
597
+ ? '(tasks.md 无 task-NN checkbox 行)'
598
+ : `已勾 ${checked}/${rows.length}:\n` + rows.slice(0, 60).join('\n') + (rows.length > 60 ? `\n(…共 ${rows.length} 条,超出 60 条截断)` : '')
599
+ } else {
600
+ tasksBlock = '(tasks.md 不存在——跳过勾选对照)'
601
+ }
602
+ } catch (e) {
603
+ tasksBlock = `(勾选状态注入失败:${e.message}——可自行读 tasks.md)`
604
+ }
605
+ promptText = promptText.replace(/\{TASKS_CHECKBOX\}/g, tasksBlock)
606
+ }
607
+
509
608
  // 决策防复潮注入(W1.1 第 3 点,task-04,FR-05):brainstorm「加载项目上下文」Step2 的
510
609
  // {DECISION_HITS} → matchKnowledge decisionHits——命中 rejected 条目时渲染「否决决策提示」段
511
610
  // (ID/标题/否决理由/复潮条件),无命中替换为空串零输出(不留残留占位符);注入结果同步落
@@ -712,22 +811,10 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
712
811
  }
713
812
  }
714
813
 
715
- // 三主阶段 Step1「进度确认」快照注入(2026-09-05 全流程审计):progress show 的核心信息
716
- // (阶段/步骤位置/推进态)CLI 渲染 prompt 时已全部在手——注入替代 agent 跑一趟 CLI 的
717
- // 工具往返(progress show 全量输出还很长)。fail-soft:异常降级为自查指引。
718
- if (['brainstorm', 'execute', 'verify'].includes(stageName) && promptText.includes('{PROGRESS_SNAPSHOT}')) {
719
- try {
720
- const done = steps.filter(s => s.status === 'completed').length
721
- const waiting = steps.filter(s => s.status === 'waiting').length
722
- const snapshot =
723
- `- 变更:${changeName || '(未知)'}\n` +
724
- `- 阶段:${stageName}(CLI 已路由到本阶段,无需再跑 progress show 确认)\n` +
725
- `- 步骤:${done}/${steps.length} 已完成${waiting > 0 ? `,${waiting} 个等待中` : ''};当前推进到第 ${stepIndex + 1} 步`
726
- promptText = promptText.split('{PROGRESS_SNAPSHOT}').join(snapshot)
727
- } catch (e) {
728
- promptText = promptText.split('{PROGRESS_SNAPSHOT}').join('(快照注入失败:' + e.message + '——可运行 sillyspec progress show 自查)')
729
- }
730
- }
814
+ // 三主阶段 Step1「进度确认」快照注入(2026-09-05 全流程审计)已随 step1 整体 noAI 化退役
815
+ // (2026-09-07):brainstorm/execute/verify 的 step1 不再渲染 prompt,快照改由
816
+ // run/progress-confirm.js 的 progressConfirm 动作 console 直出(占位符 {PROGRESS_SNAPSHOT}
817
+ // 无消费者,注入分支删除)。
731
818
 
732
819
  // execute Step「确认 worktree 路径」meta 注入(2026-09-05 全流程审计):worktree meta 由 CLI
733
820
  // 读出直接注入,agent 省一趟 worktree meta 工具往返;工具链可用性检查(真实工作)仍归 agent。
@@ -888,9 +975,9 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
888
975
  }
889
976
 
890
977
  // 完成契约(事前预知):该 stage 的机械校验通过条件,从 stage-contract-spec.js manifest 渲染,
891
- // 与 CLI 完成校验严格同源(事前给的 == 事后查的)。仅 step0 注入(与 persona/guardrails 同模式,
892
- // 一次建立即可;后续步靠 context 保留)。
893
- if (stepIndex === 0) {
978
+ // 与 CLI 完成校验严格同源(事前给的 == 事后查的)。仅首个 agent 可见步注入(与 persona/guardrails
979
+ // 同模式,一次建立即可;后续步靠 context 保留)。
980
+ if (stepIndex === firstRenderableIdx) {
894
981
  const stageContract = renderStageContract(stageName)
895
982
  if (stageContract) {
896
983
  promptText = `${promptText}\n\n${stageContract}`
@@ -898,10 +985,10 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
898
985
  }
899
986
 
900
987
  console.log(promptText)
901
- // 铁律拆分(W3 token 效率):通用流程纪律(文档优先/不跳步/不编造命令)只在首步注入——
988
+ // 铁律拆分(W3 token 效率):通用流程纪律(文档优先/不跳步/不编造命令)只在首个 agent 可见步注入——
902
989
  // 每步重复 ~800B 纯耗 context。但【平台写入规则 + 路径规则】是安全关键(防写错目录/绕过 Write),
903
990
  // 且依赖 changeName/platformOpts,必须【每步注入】(context 压缩丢失会让 agent 越界写源码)。
904
- if (stepIndex === 0) {
991
+ if (stepIndex === firstRenderableIdx) {
905
992
  console.log(`\n### ⚠️ 铁律`)
906
993
  console.log('- 文档优先:代码产出必须先有对应的设计/规范文档支撑。')
907
994
  console.log('- 聚焦本步骤:只执行本步骤描述的操作并完整做完;自行扩展或跳步会破坏状态机推进,后续步骤再做后续事。')
@@ -949,21 +1036,42 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
949
1036
  // archive-batch-31-tool-notes ②:通用 --done 模板不带该 flag,agent 照抄执行撞
950
1037
  // 「请添加 --confirm」确认门,误以为参数没带。
951
1038
  const confirmFlag = step.requiresConfirm === true ? ' --confirm' : ''
1039
+ // auto driver(2026-09-08-auto-driver D-002/P1-③):auto 模式下命令一律 run auto 形态——
1040
+ // 与 SS-META 的 doneCommand 同源(同一 cmdStage 变量),消灭「正文 run <stage> / 元数据 run auto」双命令
1041
+ const cmdStage = autoMeta ? 'auto' : stageName
1042
+ const doneCommand = `sillyspec run ${cmdStage} --done${confirmFlag}${changeFlag} --output "你的摘要"`
952
1043
  if (requiresWait) {
953
1044
  console.log(`本步骤必须等待用户输入,不能直接 --done:`)
954
- console.log(`sillyspec run ${stageName} --wait --reason "${step.waitReason || '等待用户输入'}" --options "${(step.waitOptions || ['确认']).join(',')}"${changeFlag} --output "你的问题/方案摘要"`)
1045
+ console.log(`sillyspec run ${cmdStage} --wait --reason "${step.waitReason || '等待用户输入'}" --options "${(step.waitOptions || ['确认']).join(',')}"${changeFlag} --output "你的问题/方案摘要"`)
955
1046
  console.log(``)
956
1047
  console.log(`用户回答后执行:`)
957
- console.log(`sillyspec run ${stageName} --continue --answer "用户回答"${changeFlag}`)
1048
+ console.log(`sillyspec run ${cmdStage} --continue --answer "用户回答"${changeFlag}`)
958
1049
  console.log(``)
959
1050
  console.log(`收到回答并完成本步骤总结后,再执行:`)
960
1051
  } else if (mayNeedWait) {
961
1052
  console.log(`如果需要用户决策(选择方案/确认设计等):`)
962
- console.log(`sillyspec run ${stageName} --wait --reason "${step.waitReason || '等待原因'}" --options "${(step.waitOptions || ['选项1', '选项2']).join(',')}"${changeFlag} --output "你的摘要"`)
1053
+ console.log(`sillyspec run ${cmdStage} --wait --reason "${step.waitReason || '等待原因'}" --options "${(step.waitOptions || ['选项1', '选项2']).join(',')}"${changeFlag} --output "你的摘要"`)
963
1054
  console.log(``)
964
1055
  console.log(`如果不需要用户决策,正常完成:`)
965
1056
  }
966
- console.log(`sillyspec run ${stageName} --done${confirmFlag}${changeFlag} --input "用户原始需求/反馈" --output "你的摘要"`)
1057
+ console.log(doneCommand + (autoMeta ? '' : ' --input "用户原始需求/反馈"'))
1058
+
1059
+ // ── SS-META 机器可读元数据块(change: 2026-09-08-auto-driver,D-002@v1,FR-01)──
1060
+ // 仅 auto 模式(autoMeta 非空)渲染:单行 HTML 注释内 JSON,agent 低噪可读、脚本一行正则
1061
+ // 可提取(<!--SS-META:(.*)-->)。requiresUser 四源见上方纯函数;doneCommand 与正文同源。
1062
+ if (autoMeta) {
1063
+ const meta = {
1064
+ stage: stageName,
1065
+ stepIndex: stepIndex + 1,
1066
+ stepName: step.name,
1067
+ requiresUser: requiresWait || mayNeedWait || step.requiresConfirm === true,
1068
+ doneCommand,
1069
+ waitHint: (requiresWait || mayNeedWait)
1070
+ ? 'requiresUser=true:先与用户交互(wait/continue 或 --wait-interactive 直通),再 done'
1071
+ : '直接执行任务后逐字跑 doneCommand',
1072
+ }
1073
+ console.log('<!--SS-META:' + JSON.stringify(meta) + '-->')
1074
+ }
967
1075
  }
968
1076
  /**
969
1077
  * 替换 prompt 文本中的路径根占位符 {SPEC_ROOT}/{DOCS_ROOT}/{PROJECTS_ROOT}/
@@ -209,12 +209,15 @@ export async function resolveQuickLinkedChanges({ pm, cwd, specDir, quickFiles,
209
209
  * npm test + npm run lint」从 agent 自律下沉为 CLI 卡点。
210
210
  *
211
211
  * 语义(对齐规则 8 + verify-postcheck 既有降级哲学):
212
- * - changedFiles 未触及 src/ 或 test/(纯 doc/配置改动)→ skip(不跑不阻断)
212
+ * - 变更文件未触及 src/ 或 test/(纯 doc/配置改动)→ skip(不跑不阻断)
213
213
  * - 触及 → 亲自执行 local.yaml 的 commands.test / commands.lint(复用 verify 阶段
214
214
  * 对账引擎 runVerifyTestCheck/runVerifyLintCheck,含 test_strategy/known_failures/
215
215
  * 超时语义);未配置命令 → 内部 skipped,不阻断(兼容无测试项目)
216
216
  * - 任一实测 failed → fail(调用方阻断 --done,step 回 pending)
217
217
  * - 逃生门:SILLYSPEC_QUICK_TEST_GATE=skip 显式跳过(CI/特殊场景,审计留痕)
218
+ * - 倒推 B 模式兜底(2026-09-07,quick-f9138c2f 实证):代码先行会话的文件全部早于
219
+ * 启动时间窗 → 审计 changedFiles 为空 → 此前静默 skip;--files 声明边界
220
+ * (declaredFiles = guard.allowedFiles)明确触及 src/test 时实测仍应跑。
218
221
  *
219
222
  * verify-postcheck 顶层 import 链较重(contract-matrix 等),且本函数仅 quick --done
220
223
  * 收尾路径调用——用动态 import,不进 run 命令通用启动路径(与上方 @inquirer/prompts 同款纪律)。
@@ -222,18 +225,22 @@ export async function resolveQuickLinkedChanges({ pm, cwd, specDir, quickFiles,
222
225
  * @param {object} opts
223
226
  * @param {string} opts.cwd - 仓库根(命令执行 cwd)
224
227
  * @param {string} opts.specBase - .sillyspec 目录(local.yaml 读取源,信任边界见 runVerifyTestCheck SEC-02 注释)
225
- * @param {string[]} [opts.changedFiles] - 审计口径的本轮变更文件(仓库根相对 POSIX 路径;空/null → skip)
228
+ * @param {string[]} [opts.changedFiles] - 审计口径的本轮变更文件(仓库根相对 POSIX 路径;空/null → 回退 declaredFiles)
229
+ * @param {string[]} [opts.declaredFiles] - 会话声明边界(guard.allowedFiles;changedFiles 为空时的兜底判定源)
226
230
  * @param {string} [opts.changeName] - quick 会话名(evidence-auto 策略解析用)
227
231
  * @returns {Promise<{action:'pass'|'fail'|'skip', failed:string[], reason:string, test:object|null, lint:object|null}>}
228
232
  */
229
- export async function runQuickTestLintGate({ cwd, specBase, changedFiles = [], changeName = null }) {
233
+ export async function runQuickTestLintGate({ cwd, specBase, changedFiles = [], declaredFiles = [], changeName = null }) {
230
234
  if (process.env.SILLYSPEC_QUICK_TEST_GATE === 'skip') {
231
235
  return { action: 'skip', failed: [], reason: 'SILLYSPEC_QUICK_TEST_GATE=skip 显式跳过(审计留痕)', test: null, lint: null }
232
236
  }
233
- const files = Array.isArray(changedFiles) ? changedFiles : []
237
+ const audited = Array.isArray(changedFiles) ? changedFiles : []
238
+ // 倒推 B 模式兜底:审计口径为空时回退声明边界(文件早于会话启动被基线吸收的场景)
239
+ const files = audited.length > 0 ? audited : (Array.isArray(declaredFiles) ? declaredFiles : [])
240
+ const fileSource = audited.length > 0 ? '审计' : (files.length > 0 ? '声明边界兜底(倒推 B:文件早于会话启动被基线吸收)' : '无')
234
241
  const codeFiles = files.filter(f => typeof f === 'string' && (f.startsWith('src/') || f.startsWith('test/')))
235
242
  if (files.length === 0) {
236
- return { action: 'skip', failed: [], reason: '无变更文件清单(brownfield 无 guard 或空审计),跳过', test: null, lint: null }
243
+ return { action: 'skip', failed: [], reason: `无变更文件清单(${fileSource}口径均空——brownfield 无 guard 或空审计),跳过`, test: null, lint: null }
237
244
  }
238
245
  if (codeFiles.length === 0) {
239
246
  return { action: 'skip', failed: [], reason: `纯 doc/配置改动(${files.length} 个文件均未触及 src/test,规则 8 语义跳过 test+lint)`, test: null, lint: null }
@@ -9,8 +9,8 @@
9
9
  * - executeScanPostcheck 原冗余动态 import('fs'/'path'/'child_process') 删除,改顶部静态(execSync 实际未直接用,纯遗留)
10
10
  */
11
11
  import { join, basename, extname } from 'node:path'
12
- import { existsSync, readdirSync, statSync, mkdirSync, writeFileSync, unlinkSync } from 'node:fs'
13
- import { safeGit } from './shared.js'
12
+ import { existsSync, readdirSync, statSync, mkdirSync, writeFileSync, unlinkSync, readFileSync } from 'node:fs'
13
+ import { safeGit, resolveRuntimeRoot } from './shared.js'
14
14
  import { SCAN_REQUIRED_DOCS } from '../constants.js'
15
15
 
16
16
  /**
@@ -327,6 +327,24 @@ export async function executeScanResumeCheck(cwd, platformOpts) {
327
327
  console.log(` 📂 ${p}: ${states.join(' ')} ${facts}`)
328
328
  }
329
329
  console.log('')
330
+
331
+ // IR 增量回灌 advisory(change: 2026-09-07-ir-hardening,D-006@v1,FR-03):读
332
+ // .runtime/last-delta.json(归档/delta 补跑写入的最近变更快照),14 天窗口内提示本轮
333
+ // scan 优先核对其涉及模块——advisory 不阻断不改变步骤结构;缺失/过期/解析失败静默。
334
+ try {
335
+ const sidecarPath = join(resolveRuntimeRoot(platformOpts, specBase), 'last-delta.json')
336
+ if (existsSync(sidecarPath)) {
337
+ const sidecar = JSON.parse(readFileSync(sidecarPath, 'utf8'))
338
+ const ageMs = Date.now() - Date.parse(sidecar.updatedAt || '')
339
+ const FOURTEEN_DAYS = 14 * 24 * 60 * 60 * 1000
340
+ if (Number.isFinite(ageMs) && ageMs >= 0 && ageMs <= FOURTEEN_DAYS
341
+ && Array.isArray(sidecar.affectedModules) && sidecar.affectedModules.length > 0) {
342
+ console.log(`🔁 增量回灌提示(上次归档变更 ${sidecar.change},${new Date(sidecar.updatedAt).toISOString().slice(0, 10)}):`)
343
+ console.log(` 其涉及模块 ${sidecar.affectedModules.join('、')} 的文档与 staleRefs 本轮 scan 优先核对(advisory,不据此跳过其他模块)`)
344
+ console.log('')
345
+ }
346
+ }
347
+ } catch { /* sidecar 读取/解析失败静默跳过(advisory 缺席零影响) */ }
330
348
  }
331
349
 
332
350
  /**
package/src/run/shared.js CHANGED
@@ -107,19 +107,24 @@ export function resolvePromptIncludes(text) {
107
107
  export function resolveSpecDir(cwd, opts = {}) {
108
108
  if (opts.specDir) return resolve(opts.specDir)
109
109
  const home = os.homedir()
110
- let dir = resolve(cwd)
110
+ const origin = resolve(cwd)
111
+ const originBelowHome = origin.startsWith(home + sep) // 严格子目录,不含 home 自身
112
+ let dir = origin
113
+ let passedHome = false
111
114
  while (true) {
112
- // home 拒绝守卫:cwd 本身就在 home 下(含恰好在 home 跑命令)时,home 层不匹配 .sillyspec,
113
- // 继续向上只会离项目更远(home 的父目录不可能有真项目 .sillyspec),直接回退 cwd/.sillyspec。
114
- if (dir !== home) {
115
+ // home 拒绝守卫:起点在 home 子树内时,home 层不检查 .sillyspec,
116
+ // 防止 home 子目录遍历经过 home 误命中 ~/.sillyspec;
117
+ // passedHome:经过 home 后不再检查(覆盖遍历到真实 home 的场景)。
118
+ if (!(passedHome || (dir === home && originBelowHome))) {
115
119
  const candidate = join(dir, '.sillyspec')
116
120
  if (existsSync(candidate)) return candidate
117
121
  }
122
+ if (dir === home) passedHome = true
118
123
  const parent = dirname(dir)
119
124
  if (parent === dir) break
120
125
  dir = parent
121
126
  }
122
- return join(resolve(cwd), '.sillyspec')
127
+ return join(origin, '.sillyspec')
123
128
  }
124
129
 
125
130
  /**