sillyspec 3.28.2 → 3.28.4

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 (67) hide show
  1. package/.claude/skills/sillyspec-auto/SKILL.md +16 -55
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +1 -1
  3. package/.claude/skills/sillyspec-continue/SKILL.md +8 -38
  4. package/.claude/skills/sillyspec-execute/SKILL.md +10 -2
  5. package/.claude/skills/sillyspec-knowledge/SKILL.md +19 -249
  6. package/.claude/skills/sillyspec-plan/SKILL.md +1 -1
  7. package/.claude/skills/sillyspec-propose/SKILL.md +6 -77
  8. package/.claude/skills/sillyspec-resume/SKILL.md +8 -57
  9. package/.claude/skills/sillyspec-state/SKILL.md +7 -46
  10. package/.claude/skills/sillyspec-workspace/SKILL.md +14 -156
  11. package/AGENTS.md +24 -0
  12. package/INSTRUCTIONS.md +12 -0
  13. package/package.json +1 -1
  14. package/src/archive-delta.js +127 -2
  15. package/src/change-delete.js +2 -1
  16. package/src/change-list.js +7 -2
  17. package/src/change-risk-profile.js +77 -6
  18. package/src/config-cat.js +12 -1
  19. package/src/constants.js +9 -1
  20. package/src/design-facts.js +64 -0
  21. package/src/dispatch/backends/sillyhub-mcp.js +17 -5
  22. package/src/docs-check.js +153 -27
  23. package/src/docs-migrate.js +124 -0
  24. package/src/doctor-diagnostics.js +441 -5
  25. package/src/foreign-declared.js +53 -2
  26. package/src/index.js +353 -28
  27. package/src/module-impact.js +77 -12
  28. package/src/plan-adopt-waves.js +13 -5
  29. package/src/progress/change-registry.js +35 -0
  30. package/src/progress.js +64 -1
  31. package/src/quick-recommend.js +6 -1
  32. package/src/quicklog.js +10 -2
  33. package/src/run/command.js +159 -13
  34. package/src/run/complete-handlers.js +162 -9
  35. package/src/run/complete.js +104 -5
  36. package/src/run/gates.js +116 -8
  37. package/src/run/progress-confirm.js +66 -0
  38. package/src/run/prompt.js +235 -36
  39. package/src/run/quick-audit.js +12 -5
  40. package/src/run/scan-profile.js +20 -2
  41. package/src/run/shared.js +84 -11
  42. package/src/run/stage.js +42 -0
  43. package/src/runtime-hygiene.js +84 -0
  44. package/src/scan-postcheck.js +5 -1
  45. package/src/spec-sync.js +63 -8
  46. package/src/stage-contract.js +83 -3
  47. package/src/stage-review.js +3 -0
  48. package/src/stages/archive.js +4 -4
  49. package/src/stages/brainstorm-auto.js +2 -1
  50. package/src/stages/brainstorm.js +11 -25
  51. package/src/stages/doctor.js +41 -515
  52. package/src/stages/execute.js +10 -14
  53. package/src/stages/plan-postcheck.js +191 -7
  54. package/src/stages/plan.js +7 -27
  55. package/src/stages/quick.js +15 -13
  56. package/src/stages/verify.js +23 -21
  57. package/src/sync-noise.js +158 -0
  58. package/src/sync.js +42 -17
  59. package/src/task-review.js +225 -1
  60. package/src/verify-facts-schema.js +141 -0
  61. package/src/verify-postcheck.js +421 -49
  62. package/src/verify-probes.js +142 -13
  63. package/src/workflow.js +6 -0
  64. package/src/worktree-apply.js +51 -13
  65. package/src/worktree.js +5 -1
  66. package/templates/agents-instruction.md +1 -1
  67. package/.claude/skills/verify-per-user/SKILL.md +0 -6
package/src/run/prompt.js CHANGED
@@ -19,7 +19,7 @@
19
19
  * - loadModuleContextIndex/buildModuleContextInjection 内 require('fs'/'path') 改顶部静态 import
20
20
  */
21
21
  import { basename, join } from 'node:path'
22
- import { existsSync, readFileSync, mkdirSync } from 'node:fs'
22
+ import { existsSync, readFileSync, mkdirSync, readdirSync } from 'node:fs'
23
23
  import { writeAtomicSync } from '../fs-atomic.js'
24
24
  import { stageRegistry } from '../stages/index.js'
25
25
  import { resolvePromptIncludes, resolveRuntimeRoot, safeGit, WAIT_MARKER_RE, QUICK_SID_RE } from './shared.js'
@@ -124,6 +124,63 @@ function buildModuleContextInjection(taskDescription, moduleIndex, specBase, pro
124
124
  return injection
125
125
  }
126
126
 
127
+ /**
128
+ * 读 quick session guard 的任务描述(刀①):启动 --input 由 stage.js 落 guard.taskDescription,
129
+ * 渲染 step1 模块上下文注入时作匹配源(changeName 是 quick-<hash> 无语义)。
130
+ * 与 <quicklog-id>/<linked-changes> 占位符同源同容错(session guard 优先,legacy 单文件回退)。
131
+ */
132
+ export function readQuickGuardField(changeName, specBase, field) {
133
+ try {
134
+ const sessionGuardFile = join(specBase, '.runtime', 'quick-sessions', changeName, 'guard.json')
135
+ const legacyGuardFile = join(specBase, '.runtime', 'quick-guard.json')
136
+ const guard = existsSync(sessionGuardFile)
137
+ ? JSON.parse(readFileSync(sessionGuardFile, 'utf8'))
138
+ : (existsSync(legacyGuardFile) ? JSON.parse(readFileSync(legacyGuardFile, 'utf8')) : null)
139
+ return guard ? (guard[field] || '') : ''
140
+ } catch {
141
+ return ''
142
+ }
143
+ }
144
+
145
+ /**
146
+ * quick step1 项目上下文摘要(刀①):CLI 代读 projects/*.yaml 与 CONVENTIONS.md 开头注入
147
+ * prompt——替代 agent 两轮 cat(quick 是最高频路径,省 token + 省轮次)。fail-soft:
148
+ * 读不到落单行说明,绝不阻断 prompt 输出。
149
+ */
150
+ export function buildQuickContextDigest(specBase, projectName) {
151
+ try {
152
+ const sections = []
153
+ // 项目登记摘要(name/path/status 一行一个——技术栈等扩展字段按行透传 top-level 短行)
154
+ const projectsDir = join(specBase, 'projects')
155
+ if (existsSync(projectsDir)) {
156
+ const lines = []
157
+ for (const f of readdirSync(projectsDir).filter(f => f.endsWith('.yaml'))) {
158
+ const text = readFileSync(join(projectsDir, f), 'utf8')
159
+ const name = (text.match(/^name:\s*(.+)$/m) || [])[1]
160
+ const path = (text.match(/^path:\s*(.+)$/m) || [])[1]
161
+ const status = (text.match(/^status:\s*(.+)$/m) || [])[1]
162
+ lines.push(`- ${name ? name.trim() : f.replace(/\.yaml$/, '')}(${path ? path.trim() : '?'})${status ? ` — ${status.trim()}` : ''}`)
163
+ }
164
+ if (lines.length > 0) sections.push('### 项目登记(projects/*.yaml 摘要)\n' + lines.join('\n'))
165
+ }
166
+ // CONVENTIONS.md 开头(截断保底——全文可能很长,注入是省读取税不是塞全文)
167
+ const convPath = join(specBase, 'docs', projectName, 'scan', 'CONVENTIONS.md')
168
+ if (existsSync(convPath)) {
169
+ const text = readFileSync(convPath, 'utf8')
170
+ const HEAD_CHARS = 1200
171
+ const head = text.length > HEAD_CHARS
172
+ ? text.slice(0, HEAD_CHARS) + `\n…(截断——全文 ${convPath},需要时再读)`
173
+ : text
174
+ sections.push('### 项目约定(CONVENTIONS.md 开头,CLI 代读)\n' + head)
175
+ }
176
+ return sections.length > 0
177
+ ? sections.join('\n\n')
178
+ : '(无项目登记/约定文档——需要时按 prompt 内路径自查)'
179
+ } catch (e) {
180
+ return `(项目上下文注入失败:${e && e.message ? e.message : e}——需要时再 cat {SPEC_ROOT}/projects/*.yaml 与 CONVENTIONS.md)`
181
+ }
182
+ }
183
+
127
184
  // parseModuleMapSimple 复用 modules.js 的 canonical 实现(合并历史 copy-paste 副本,2026-08-07;
128
185
  // 无循环依赖:modules.js 仅 import fs/path/db.js,prompt.js → modules.js 单向)。
129
186
  /**
@@ -185,7 +242,20 @@ export function collectStageWaitHistory(progress, stageName) {
185
242
  /**
186
243
  * 输出当前步骤的 prompt
187
244
  */
188
- export async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjectName, platformOpts = {}, prevStepAnswer = null, waitHistory = null) {
245
+ /**
246
+ * requiresUser —— auto driver 元数据(change: 2026-09-08-auto-driver,D-002@v1)的四源判定纯函数:
247
+ * step.requiresWait / conditionalWait / requiresConfirm 三键(step 定义权威信号)+ WAIT_MARKER_RE 正文扫描
248
+ * (execute/verify 的 step 定义无三键,纯三源会漏真 wait 步)。全缺兜底 **false**——auto 默认语义是
249
+ * 继续驱动(Grill P1-①:保守 true 会让 execute/verify 逐步人工确认,推翻 FR-01)。
250
+ * 与 outputStep 内既有 mayNeedWait 计算同源(:1019 附近),仅多 requiresConfirm 一源。
251
+ */
252
+ export function requiresUser(step, promptText) {
253
+ if (!step) return false
254
+ if (step.requiresWait === true || step.conditionalWait === true || step.requiresConfirm === true) return true
255
+ return WAIT_MARKER_RE.test(String(promptText || ''))
256
+ }
257
+
258
+ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, dbProjectName, platformOpts = {}, prevStepAnswer = null, waitHistory = null, autoMeta = null) {
189
259
  const step = steps[stepIndex]
190
260
  const total = steps.length
191
261
  // ── 越界防御 ──
@@ -199,6 +269,16 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
199
269
  console.error(` 请检查 --change 指定的变更目录是否存在、specRoot 是否正确,然后重试。`)
200
270
  return false
201
271
  }
272
+
273
+ // ── 首个 agent 可见步索引(「首步一次建立」类注入的锚点)──
274
+ // step0 为 noAI(brainstorm/execute/verify 的 step1「进度确认」,2026-09-07)时不渲染 prompt,
275
+ // persona/护栏/完成契约/铁律/平台路径约束等 step0 专属注入须顺延到首个渲染步,否则整段丢失
276
+ //(spec-dir.test.mjs Test4 平台 specDir 断言即此坑的行为探针)。全 noAI 防御性回退 0。
277
+ const firstRenderableIdx = (() => {
278
+ const i = steps.findIndex(s => s?.noAI !== true)
279
+ return i === -1 ? 0 : i
280
+ })()
281
+
202
282
  const projectName = dbProjectName || basename(cwd)
203
283
 
204
284
  // ── Revision context injection ──
@@ -244,8 +324,8 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
244
324
  console.log(`changeDir: ${changeDir}`)
245
325
  }
246
326
  console.log(`---\n`)
247
- // persona 只在 stage 首步注入(step0)——角色设定一次即可,后续 step 重复注入纯属 token 浪费
248
- if (personas[stageName] && stepIndex === 0) {
327
+ // persona 只在首个 agent 可见步注入——角色设定一次即可,后续 step 重复注入纯属 token 浪费
328
+ if (personas[stageName] && stepIndex === firstRenderableIdx) {
249
329
  console.log(personas[stageName])
250
330
  console.log('')
251
331
  }
@@ -255,7 +335,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
255
335
 
256
336
  console.log(`## Step ${stepIndex + 1}/${total}: ${step.name}\n`)
257
337
  if (guardrails) {
258
- if (stepIndex === 0) {
338
+ if (stepIndex === firstRenderableIdx) {
259
339
  console.log(guardrails.trim())
260
340
  console.log('')
261
341
  } else {
@@ -398,13 +478,15 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
398
478
  platformDirectives.push(`workspace_id: ${platformOpts.workspaceId}`)
399
479
  }
400
480
  // 平台 directives 前置条件:
401
- // - step0(对齐 persona 仅 step0 策略):路径列表/Write 规则/占位符映射一次建立即可;
481
+ // - 首个 agent 可见步(对齐 persona 仅 step0 策略):路径列表/Write 规则/占位符映射
482
+ // 一次建立即可——step0 为 noAI(brainstorm/execute/verify 的 step1「进度确认」,
483
+ // 2026-09-07)时首个可见步顺延,按 defSteps 计算(坑同 scan quick 档 noAI preflight);
402
484
  // - scan 阶段所有 step:scan 的 profileDirectives 本就每步注入(见下方 L917),平台路径
403
485
  // 约束也需每步可见——否则 quick profile 的 step0 是 noAI preflight(自动执行),run scan
404
486
  // 输出的首个可见 step 是 step1,会漏掉 specDir 路径,导致平台模式写入约束丢失。
405
487
  // 后续 step 靠 changeDir header + footer 平台规则(L1077+)维持,platformDirectives
406
488
  // 每步重复约 500 tokens 仅在 scan(步数有限)可接受。
407
- if (stepIndex === 0 || stageName === 'scan') {
489
+ if (stepIndex === firstRenderableIdx || stageName === 'scan') {
408
490
  promptText = platformDirectives.join('\n') + '\n\n' + promptText
409
491
  }
410
492
  } else {
@@ -506,6 +588,88 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
506
588
  promptText = promptText.replace(/\{SCAN_FACTS\}/g, factsInjected)
507
589
  }
508
590
 
591
+ // ── 机械事实注入三件(2026-09-07 注入缺口批次):{LOCAL_COMMANDS} / {GIT_DIRTY} / {TASKS_CHECKBOX} ──
592
+ // CLI 渲染 prompt 时已能确定性拿到的机械事实,替代 agent 各自跑一趟 cat local.yaml /
593
+ // git status / 手数勾选(全流程 7+ 处步骤的高频机械读)。全部 fail-soft:异常注入单行
594
+ // 说明,不留残留占位符;替换后占位符即消失(不存在的档位自然零输出)。
595
+
596
+ // ① {LOCAL_COMMANDS}:local.yaml commands 段原文(brainstorm/plan/execute/verify/quick 多步消费)
597
+ if (promptText.includes('{LOCAL_COMMANDS}')) {
598
+ let localBlock = ''
599
+ try {
600
+ const lcSpecBase = resolvePromptSpecBase(platformOpts, cwd)
601
+ const localPath = join(lcSpecBase, 'local.yaml')
602
+ if (existsSync(localPath)) {
603
+ const lcLines = String(readFileSync(localPath, 'utf8')).replace(/\r\n?/g, '\n').split('\n')
604
+ const start = lcLines.findIndex(l => /^commands:\s*(#.*)?$/.test(l))
605
+ if (start !== -1) {
606
+ const body = []
607
+ for (let i = start + 1; i < lcLines.length && !/^\S/.test(lcLines[i]); i++) body.push(lcLines[i])
608
+ // unavailable 标记的条目按未配置语义剔除行(与 extractTestCommand 同口径)
609
+ const avail = body.filter(l => !/^\s*[\w.-]+:\s*['"]?unavailable['"]?\s*(#.*)?$/i.test(l))
610
+ const trimmed = avail.join('\n').trim()
611
+ localBlock = trimmed
612
+ ? 'commands:\n' + trimmed
613
+ : '(local.yaml 存在但 commands 段为空——可跑 `sillyspec local detect` 补默认骨架)'
614
+ } else {
615
+ localBlock = '(local.yaml 存在但无 commands 段——可跑 `sillyspec local detect` 补默认骨架)'
616
+ }
617
+ } else {
618
+ localBlock = '(未配置 local.yaml——先跑 `sillyspec local detect` 生成骨架,以命令输出为准)'
619
+ }
620
+ } catch (e) {
621
+ localBlock = `(构建命令注入失败:${e.message}——可自行读 local.yaml)`
622
+ }
623
+ promptText = promptText.replace(/\{LOCAL_COMMANDS\}/g, localBlock)
624
+ }
625
+
626
+ // ①b {QUICK_CONTEXT_DIGEST}(刀①,2026-09-08):quick step1 的项目/约定上下文 CLI 代读注入
627
+ // ——替代 agent cat projects/*.yaml 与 CONVENTIONS.md 两轮文件读取(quick 是最高频路径)。
628
+ // fail-soft 同三件套;无 quick 占位符自然零输出。
629
+ if (stageName === 'quick' && promptText.includes('{QUICK_CONTEXT_DIGEST}')) {
630
+ const digestSpecBase = resolvePromptSpecBase(platformOpts, cwd)
631
+ promptText = promptText.replace(/\{QUICK_CONTEXT_DIGEST\}/g, buildQuickContextDigest(digestSpecBase, projectName))
632
+ }
633
+
634
+ // ② {GIT_DIRTY}:工作区脏文件清单(git status --porcelain,quick 收尾步消费)
635
+ if (promptText.includes('{GIT_DIRTY}')) {
636
+ let dirtyBlock = ''
637
+ try {
638
+ const { gitQuiet } = await import('../git-helper.js')
639
+ // gitQuiet 返回裸 string(失败 null)——非 safeGit 的 {value,error} 形状
640
+ const lines = String(gitQuiet(cwd, ['status', '--porcelain']) || '').split('\n').filter(Boolean)
641
+ dirtyBlock = lines.length === 0
642
+ ? '(工作区干净,无脏文件)'
643
+ : lines.slice(0, 40).join('\n') + (lines.length > 40 ? `\n(…共 ${lines.length} 行,超出 40 行截断——完整清单可自行跑 git status --porcelain)` : '')
644
+ } catch (e) {
645
+ dirtyBlock = `(脏文件注入失败:${e.message}——可自行跑 git status --porcelain)`
646
+ }
647
+ promptText = promptText.replace(/\{GIT_DIRTY\}/g, dirtyBlock)
648
+ }
649
+
650
+ // ③ {TASKS_CHECKBOX}:tasks.md 任务勾选状态投影(verify 逐项检查步消费——省 agent 手数)
651
+ if (promptText.includes('{TASKS_CHECKBOX}')) {
652
+ let tasksBlock = ''
653
+ try {
654
+ const tcSpecBase = resolvePromptSpecBase(platformOpts, cwd)
655
+ const tasksPath = changeName ? join(tcSpecBase, 'changes', changeName, 'tasks.md') : null
656
+ if (tasksPath && existsSync(tasksPath)) {
657
+ const re = /^(\s*[-*]\s+\[([ xX])\]\s+task-\d+.*)$/
658
+ const rows = String(readFileSync(tasksPath, 'utf8')).replace(/\r\n?/g, '\n').split('\n')
659
+ .map(l => l.match(re)).filter(Boolean).map(m => m[1].trim())
660
+ const checked = rows.filter(r => /\[x\]/i.test(r)).length
661
+ tasksBlock = rows.length === 0
662
+ ? '(tasks.md 无 task-NN checkbox 行)'
663
+ : `已勾 ${checked}/${rows.length}:\n` + rows.slice(0, 60).join('\n') + (rows.length > 60 ? `\n(…共 ${rows.length} 条,超出 60 条截断)` : '')
664
+ } else {
665
+ tasksBlock = '(tasks.md 不存在——跳过勾选对照)'
666
+ }
667
+ } catch (e) {
668
+ tasksBlock = `(勾选状态注入失败:${e.message}——可自行读 tasks.md)`
669
+ }
670
+ promptText = promptText.replace(/\{TASKS_CHECKBOX\}/g, tasksBlock)
671
+ }
672
+
509
673
  // 决策防复潮注入(W1.1 第 3 点,task-04,FR-05):brainstorm「加载项目上下文」Step2 的
510
674
  // {DECISION_HITS} → matchKnowledge decisionHits——命中 rejected 条目时渲染「否决决策提示」段
511
675
  // (ID/标题/否决理由/复潮条件),无命中替换为空串零输出(不留残留占位符);注入结果同步落
@@ -712,22 +876,10 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
712
876
  }
713
877
  }
714
878
 
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
- }
879
+ // 三主阶段 Step1「进度确认」快照注入(2026-09-05 全流程审计)已随 step1 整体 noAI 化退役
880
+ // (2026-09-07):brainstorm/execute/verify 的 step1 不再渲染 prompt,快照改由
881
+ // run/progress-confirm.js 的 progressConfirm 动作 console 直出(占位符 {PROGRESS_SNAPSHOT}
882
+ // 无消费者,注入分支删除)。
731
883
 
732
884
  // execute Step「确认 worktree 路径」meta 注入(2026-09-05 全流程审计):worktree meta 由 CLI
733
885
  // 读出直接注入,agent 省一趟 worktree meta 工具往返;工具链可用性检查(真实工作)仍归 agent。
@@ -840,13 +992,39 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
840
992
  promptText = promptText.split('{EVIDENCE_AUTO_RECOMMENDATION}').join(eaInjected)
841
993
  }
842
994
 
843
- // 注入模块上下文(brainstorm/plan/execute 阶段,基于 Module Context Index)
844
- if (['brainstorm', 'plan', 'execute'].includes(stageName) && projectName) {
995
+ // {ARCHIVE_IMPACT_AUDIT}(2026-09-09 ql-20260909-004):archive extract-module-impact 步的
996
+ // 三重核对机械预填——auditModuleImpactAgainstDiff 代算(module-impact × diff × map 归属),
997
+ // agent 只裁决不一致项。fail-soft:审计异常降级单行指引(回退手跑 git diff)。
998
+ if (stageName === 'archive' && promptText.includes('{ARCHIVE_IMPACT_AUDIT}')) {
999
+ let auditBlock = ''
1000
+ try {
1001
+ const { auditModuleImpactAgainstDiff } = await import('../archive-delta.js')
1002
+ const auditSpecBase = resolvePromptSpecBase(platformOpts, cwd)
1003
+ const r = auditModuleImpactAgainstDiff({ cwd, changeName, specDir: auditSpecBase })
1004
+ auditBlock = [
1005
+ '【三重核对报告(CLI 机械预填)】',
1006
+ r.summary,
1007
+ ...(r.mismatches.length > 0 ? ['不一致项:', ...r.mismatches.map(m => ' - ' + m), '→ 逐项裁决并修正 module-impact.md;其余(未列出的 diff 文件为 .sillyspec 产物不参与核对)无需处理。'] : ['一致 ✓——第 3 步修正可跳过。']),
1008
+ ].join(String.fromCharCode(10))
1009
+ } catch (e) {
1010
+ auditBlock = '(三重核对机械预填失败:' + (e && e.message ? e.message : e) + '——回退手跑 git diff --name-only 比对)'
1011
+ }
1012
+ promptText = promptText.split('{ARCHIVE_IMPACT_AUDIT}').join(auditBlock)
1013
+ }
1014
+
1015
+ // 注入模块上下文(brainstorm/plan/execute 阶段全步 + quick 首步「理解任务」——刀①:quick
1016
+ // step1 原让 agent cat module-map 再挑模块卡读,改为按任务描述匹配后注入,基于 Module Context Index)
1017
+ const quickFirstStep = stageName === 'quick' && step && step.name === '理解任务'
1018
+ if ((['brainstorm', 'plan', 'execute'].includes(stageName) || quickFirstStep) && projectName) {
845
1019
  const effectiveSpecBase = resolvePromptSpecBase(platformOpts, cwd)
846
1020
  const moduleIndex = loadModuleContextIndex(effectiveSpecBase, projectName)
847
1021
  if (moduleIndex && Object.keys(moduleIndex).length > 0) {
848
- // 尝试从 step prompt / changeName 匹配模块
849
- const taskDesc = step.prompt || changeName || ''
1022
+ // 尝试从 step prompt / changeName 匹配模块;quick 用 guard.taskDescription(启动 --input)——
1023
+ // changeName 是 quick-<hash> 无语义、step.prompt 全文噪音大(刀①)
1024
+ let taskDesc = step.prompt || changeName || ''
1025
+ if (stageName === 'quick') {
1026
+ taskDesc = readQuickGuardField(changeName, effectiveSpecBase, 'taskDescription') || taskDesc
1027
+ }
850
1028
  const injection = buildModuleContextInjection(taskDesc, moduleIndex, effectiveSpecBase, projectName)
851
1029
  if (injection) {
852
1030
  promptText = injection + '\n' + promptText
@@ -888,9 +1066,9 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
888
1066
  }
889
1067
 
890
1068
  // 完成契约(事前预知):该 stage 的机械校验通过条件,从 stage-contract-spec.js manifest 渲染,
891
- // 与 CLI 完成校验严格同源(事前给的 == 事后查的)。仅 step0 注入(与 persona/guardrails 同模式,
892
- // 一次建立即可;后续步靠 context 保留)。
893
- if (stepIndex === 0) {
1069
+ // 与 CLI 完成校验严格同源(事前给的 == 事后查的)。仅首个 agent 可见步注入(与 persona/guardrails
1070
+ // 同模式,一次建立即可;后续步靠 context 保留)。
1071
+ if (stepIndex === firstRenderableIdx) {
894
1072
  const stageContract = renderStageContract(stageName)
895
1073
  if (stageContract) {
896
1074
  promptText = `${promptText}\n\n${stageContract}`
@@ -898,10 +1076,10 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
898
1076
  }
899
1077
 
900
1078
  console.log(promptText)
901
- // 铁律拆分(W3 token 效率):通用流程纪律(文档优先/不跳步/不编造命令)只在首步注入——
1079
+ // 铁律拆分(W3 token 效率):通用流程纪律(文档优先/不跳步/不编造命令)只在首个 agent 可见步注入——
902
1080
  // 每步重复 ~800B 纯耗 context。但【平台写入规则 + 路径规则】是安全关键(防写错目录/绕过 Write),
903
1081
  // 且依赖 changeName/platformOpts,必须【每步注入】(context 压缩丢失会让 agent 越界写源码)。
904
- if (stepIndex === 0) {
1082
+ if (stepIndex === firstRenderableIdx) {
905
1083
  console.log(`\n### ⚠️ 铁律`)
906
1084
  console.log('- 文档优先:代码产出必须先有对应的设计/规范文档支撑。')
907
1085
  console.log('- 聚焦本步骤:只执行本步骤描述的操作并完整做完;自行扩展或跳步会破坏状态机推进,后续步骤再做后续事。')
@@ -949,21 +1127,42 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
949
1127
  // archive-batch-31-tool-notes ②:通用 --done 模板不带该 flag,agent 照抄执行撞
950
1128
  // 「请添加 --confirm」确认门,误以为参数没带。
951
1129
  const confirmFlag = step.requiresConfirm === true ? ' --confirm' : ''
1130
+ // auto driver(2026-09-08-auto-driver D-002/P1-③):auto 模式下命令一律 run auto 形态——
1131
+ // 与 SS-META 的 doneCommand 同源(同一 cmdStage 变量),消灭「正文 run <stage> / 元数据 run auto」双命令
1132
+ const cmdStage = autoMeta ? 'auto' : stageName
1133
+ const doneCommand = `sillyspec run ${cmdStage} --done${confirmFlag}${changeFlag} --output "你的摘要"`
952
1134
  if (requiresWait) {
953
1135
  console.log(`本步骤必须等待用户输入,不能直接 --done:`)
954
- console.log(`sillyspec run ${stageName} --wait --reason "${step.waitReason || '等待用户输入'}" --options "${(step.waitOptions || ['确认']).join(',')}"${changeFlag} --output "你的问题/方案摘要"`)
1136
+ console.log(`sillyspec run ${cmdStage} --wait --reason "${step.waitReason || '等待用户输入'}" --options "${(step.waitOptions || ['确认']).join(',')}"${changeFlag} --output "你的问题/方案摘要"`)
955
1137
  console.log(``)
956
1138
  console.log(`用户回答后执行:`)
957
- console.log(`sillyspec run ${stageName} --continue --answer "用户回答"${changeFlag}`)
1139
+ console.log(`sillyspec run ${cmdStage} --continue --answer "用户回答"${changeFlag}`)
958
1140
  console.log(``)
959
1141
  console.log(`收到回答并完成本步骤总结后,再执行:`)
960
1142
  } else if (mayNeedWait) {
961
1143
  console.log(`如果需要用户决策(选择方案/确认设计等):`)
962
- console.log(`sillyspec run ${stageName} --wait --reason "${step.waitReason || '等待原因'}" --options "${(step.waitOptions || ['选项1', '选项2']).join(',')}"${changeFlag} --output "你的摘要"`)
1144
+ console.log(`sillyspec run ${cmdStage} --wait --reason "${step.waitReason || '等待原因'}" --options "${(step.waitOptions || ['选项1', '选项2']).join(',')}"${changeFlag} --output "你的摘要"`)
963
1145
  console.log(``)
964
1146
  console.log(`如果不需要用户决策,正常完成:`)
965
1147
  }
966
- console.log(`sillyspec run ${stageName} --done${confirmFlag}${changeFlag} --input "用户原始需求/反馈" --output "你的摘要"`)
1148
+ console.log(doneCommand + (autoMeta ? '' : ' --input "用户原始需求/反馈"'))
1149
+
1150
+ // ── SS-META 机器可读元数据块(change: 2026-09-08-auto-driver,D-002@v1,FR-01)──
1151
+ // 仅 auto 模式(autoMeta 非空)渲染:单行 HTML 注释内 JSON,agent 低噪可读、脚本一行正则
1152
+ // 可提取(<!--SS-META:(.*)-->)。requiresUser 四源见上方纯函数;doneCommand 与正文同源。
1153
+ if (autoMeta) {
1154
+ const meta = {
1155
+ stage: stageName,
1156
+ stepIndex: stepIndex + 1,
1157
+ stepName: step.name,
1158
+ requiresUser: requiresWait || mayNeedWait || step.requiresConfirm === true,
1159
+ doneCommand,
1160
+ waitHint: (requiresWait || mayNeedWait)
1161
+ ? 'requiresUser=true:先与用户交互(wait/continue 或 --wait-interactive 直通),再 done'
1162
+ : '直接执行任务后逐字跑 doneCommand',
1163
+ }
1164
+ console.log('<!--SS-META:' + JSON.stringify(meta) + '-->')
1165
+ }
967
1166
  }
968
1167
  /**
969
1168
  * 替换 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
  /**
@@ -333,6 +338,8 @@ export function resolveQuickSessionsDir(platformOpts, localSpecBase) {
333
338
  // GHOST_EMPTY_DIR_STALE_MS 同口径(7 天)——正常并发会话间隔在分钟/小时级,7 天未收尾的
334
339
  // 会话按僵尸处理,防崩溃残留的 guard 永久「认领」文件侵蚀危险门。
335
340
  const FOREIGN_SESSION_STALE_MS = 7 * 24 * 60 * 60 * 1000
341
+ // 空壳会话点名宽限期:零步骤完成但启动未满此时长的不列(进行中会话的正常起步态)
342
+ const EMPTY_SHELL_GRACE_MS = 10 * 60 * 1000
336
343
 
337
344
  /**
338
345
  * 枚举其他 active quick 会话的显式文件声明(坑 foreign-session-declared-false-block)。
@@ -419,6 +426,9 @@ export function detectEmptyShellQuickSessions(platformOpts, localSpecBase, curre
419
426
  if (!guard || !guard.startedAt) continue
420
427
  const startedAtMs = Date.parse(guard.startedAt)
421
428
  if (!Number.isFinite(startedAtMs) || nowMs - startedAtMs > FOREIGN_SESSION_STALE_MS) continue
429
+ // 宽限期(2026-09-09 §7-4,会话实证:2 分钟前刚启动的进行中会话被点名疑似空壳):
430
+ // 零步骤完成对刚起步的会话是正常态,不满 10 分钟不列(宁可漏报不误报的同款取向)。
431
+ if (nowMs - startedAtMs < EMPTY_SHELL_GRACE_MS) continue
422
432
  try {
423
433
  const p = pm.read(cwd, sessionName)
424
434
  if (!p || !p.stages) continue
@@ -467,6 +477,16 @@ export function writePlatformPointer(cwd, platformOpts, extra = {}) {
467
477
  console.warn(`⚠️ 检测到自指平台指针(repo-native junction 回环,specRoot 指回本地 .sillyspec),已跳过指针写入并按本地模式运行: ${platformOpts.specRoot}`)
468
478
  return false
469
479
  }
480
+ // temp 残留写侧守卫(2026-09-08 temp 投毒治理,防患于未然):specRoot 在系统 temp 且
481
+ // 项目 cwd 不在 temp → 拒绝三写。真实平台 spec 根永不在 temp;此类组合只可能来自
482
+ // 测试/联调以临时 specRoot 在真实项目根跑平台模式命令——指针/声明一旦落盘,指针被
483
+ // 清理后残留声明会让全命令 fail-closed(读侧 isTempResidueSpecRoot 降级兜底已存在
484
+ // 的污染,此处从源头断新增)。cwd 同在 temp(套件隔离正常形态)不拦,平台模式
485
+ // 测试不受影响。
486
+ if (isTempResidueSpecRoot(cwd, platformOpts.specRoot)) {
487
+ console.warn(`⚠️ 拒绝写入平台指针:specRoot 指向系统 temp 目录(${platformOpts.specRoot})而项目不在 temp 下——测试/联调残留特征,已跳过三写并按本地模式运行`)
488
+ return false
489
+ }
470
490
  // HUB-05:合并保留既有生命周期字段。scan 完成时指针被写入 status/completedAt/scanStatus
471
491
  //(complete-handlers),但下一次任何平台模式 run(含只读 --status)都会重写指针——
472
492
  // 恢复链只回填 specRoot 等四字段,不保留 status → 指针永远回 active、isPointerStale
@@ -564,6 +584,38 @@ export function isSelfReferentialSpecRoot(cwd, specRoot) {
564
584
  }
565
585
  }
566
586
 
587
+ /**
588
+ * temp 残留 specRoot 判定(temp 投毒治理,2026-09-08:multi-agent-platform 工作台
589
+ * 「总览不可用」排障实证——Git Bash mktemp 临时 specRoot 的平台模式命令在真实项目根
590
+ * 写下指针+声明,指针随后被 cleanup/STALE 清理,声明按设计存活(唯一删除路径 =
591
+ * platform disconnect)→ 全命令 fail-closed,且 disconnect 会连带清 local.yaml
592
+ * platform 段误伤真实平台连接)。
593
+ *
594
+ * 判定:specRoot 位于系统 temp 目录 **且** cwd 不在 temp 下 → true。两个条件缺一不可:
595
+ * 真实平台的 spec 根永不在 temp(daemon 用 ~/.sillyhub/daemon/specs/{ws},服务器用
596
+ * /data/spec-workspaces);测试/联调的 temp specRoot 只允许配 temp 项目 cwd(套件隔离
597
+ * 正常形态,见 test/run-tests.mjs childEnv 与 platform-managed-declaration 夹具——
598
+ * cwd 与 spec 同在 temp 下时本函数返回 false,既有 fail-closed 测试语义不变)。
599
+ *
600
+ * 与 isSelfReferentialSpecRoot 并列为声明的两类「可证伪残留」:自指回环(repo-native
601
+ * junction 投毒)已有降级先例(run/command.js:429),本函数补 temp 残留这一类。
602
+ * 口径对齐 progress.js 指针路径的 temp 警告(resolve() 前缀比较,不 realpath——temp
603
+ * 判定是残留启发式,无需穿透 junction 的强一致)。
604
+ *
605
+ * @param {string} cwd - 项目根(与 isSelfReferentialSpecRoot 同基准)
606
+ * @param {string|null} [specRoot] - 平台 specRoot(可 null/undefined/空串)
607
+ * @returns {boolean} 是否 temp 残留(写侧应拒绝三写,读侧应降级本地并清理声明)
608
+ */
609
+ export function isTempResidueSpecRoot(cwd, specRoot) {
610
+ if (!specRoot || typeof specRoot !== 'string') return false
611
+ const tmp = resolve(os.tmpdir())
612
+ const sr = resolve(specRoot)
613
+ if (sr !== tmp && !sr.startsWith(tmp + sep)) return false
614
+ const c = resolve(cwd)
615
+ if (c === tmp || c.startsWith(tmp + sep)) return false
616
+ return true
617
+ }
618
+
567
619
  /**
568
620
  * 平台模式判定单一数据源(自指免疫,变更 2026-08-23-repo-native-spec-backfill)。
569
621
  *
@@ -769,7 +821,8 @@ export async function triggerPull(cwd, changeName, platformOpts = {}, opts = {})
769
821
  if (!sm._getPlatform()) return
770
822
  if (_autoPullRecently(cwd)) return
771
823
  _stampAutoPull(cwd)
772
- await raceWithAbort((sig) => sm.pull(changeName, { skipIfLocalDirty: true, signal: sig.signal }), opts.timeoutMs ?? AUTO_PULL_TIMEOUT_MS)
824
+ // autoPull:噪音闸标识(sync.js pull 的失败 warn 走跨进程静默窗口,见 sync-noise.js)
825
+ await raceWithAbort((sig) => sm.pull(changeName, { skipIfLocalDirty: true, signal: sig.signal, autoPull: true }), opts.timeoutMs ?? AUTO_PULL_TIMEOUT_MS)
773
826
  } catch (e) {
774
827
  // pull 失败静默跳过(Best Effort,失败不影响正确性)
775
828
  console.warn('⚠️ 拉取失败:', e.message)
@@ -808,9 +861,10 @@ export async function triggerPullActiveChange(cwd, platformOpts = {}) {
808
861
  }
809
862
  if (!cn) return
810
863
  // 已确认连接 + 单活跃变更,调 pull(自动注入 2s 熔断 + abort(HUB-09);skipIfLocalDirty 保守守卫
811
- // 同 triggerPull——本地脏时跳过 import,防平台旧快照覆盖本地领先进度,ql-20260818-008)
864
+ // 同 triggerPull——本地脏时跳过 import,防平台旧快照覆盖本地领先进度,ql-20260818-008)。
865
+ // autoPull:噪音闸标识(sync.js pull 的失败 warn 走跨进程静默窗口,见 sync-noise.js)
812
866
  try {
813
- await raceWithAbort((sig) => sm.pull(cn, { skipIfLocalDirty: true, signal: sig.signal }), AUTO_PULL_TIMEOUT_MS)
867
+ await raceWithAbort((sig) => sm.pull(cn, { skipIfLocalDirty: true, signal: sig.signal, autoPull: true }), AUTO_PULL_TIMEOUT_MS)
814
868
  } catch (e) {
815
869
  console.warn('⚠️ 拉取失败:', e.message)
816
870
  }
@@ -879,8 +933,9 @@ export function parsePorcelainPath(line) {
879
933
  }
880
934
 
881
935
  /**
882
- * quick 自身写入的 .sillyspec/ 元数据判定(auditQuickCompletion 审计 与 QUICKLOG「文件:」行回填
883
- * 单源)。回填文件行时复用:review.changedFiles 含这些元数据时过滤掉,只留真实业务文件。
936
+ * quick 自身写入的 .sillyspec/ 元数据判定(auditQuickCompletion 审计豁免的单一真相源)。
937
+ * 注意:QUICKLOG「文件:」行回填**不再直接复用**本谓词——记录面与审计豁免面已分叉
938
+ * (模块卡 + changelog sidecar 审计照旧放行、但要进文件行),见 isQuicklogFileLineNoise。
884
939
  *
885
940
  * 归类口径:
886
941
  * - quicklog/.runtime/modules/_module-map/knowledge-uncategorized 等 quick 自身产物 → 元数据
@@ -889,7 +944,7 @@ export function parsePorcelainPath(line) {
889
944
  *
890
945
  * 「并发工作 vs 偷建变更」的意图软判定留给 sillyhub,确定性校验只做路径归类。
891
946
  *
892
- * @param {string} p 文件路径(容错 \\ / 混用)
947
+ * @param {string} p 文件路径(容错 \ / 混用)
893
948
  * @param {string[]} [linkedChanges] 关联变更名列表(影响 changes/ 归类)
894
949
  * @returns {boolean} true=quick 元数据,应从业务文件列表过滤掉
895
950
  */
@@ -910,6 +965,24 @@ export function isQuickMetadata(p, linkedChanges = []) {
910
965
  return false
911
966
  }
912
967
 
968
+ /**
969
+ * QUICKLOG「文件:」行回填过滤——isQuickMetadata 的**记录面**变体(2026-09-08 用户反馈①:
970
+ * 审计按 .sillyspec/ 放行规则整体豁免没问题,但模块卡 + changelog sidecar 恰是收尾必改项,
971
+ * 被同一谓词从文件行滤掉后每回都得手工补录)。审计豁免面(isQuickMetadata)与记录面在此
972
+ * 分叉:quick 自身元数据(quicklog/.runtime/知识库未归类)照旧不入文件行,但本 quick 实际
973
+ * 改动的模块卡(`modules/<id>.md`)与 sidecar(`modules/<id>.changelog.md`,同被上方
974
+ * `[^/]+\.md$` 覆盖)保留——它们是真实收尾工作的一部分。`_module-map.yaml` 仍滤:CLI
975
+ * (module-resolve)维护的索引,非 agent 手工改动。
976
+ * @param {string} p 文件路径(容错 \ / 混用)
977
+ * @param {string[]} [linkedChanges] 关联变更名列表(透传 isQuickMetadata)
978
+ * @returns {boolean} true=元数据噪音,应从「文件:」行过滤掉
979
+ */
980
+ export function isQuicklogFileLineNoise(p, linkedChanges = []) {
981
+ const file = String(p).replace(/\\/g, '/')
982
+ if (/^\.sillyspec\/docs\/[^/]+\/modules\/[^/]+\.md$/.test(file)) return false
983
+ return isQuickMetadata(p, linkedChanges)
984
+ }
985
+
913
986
  // 危险文件清单(auditQuickCompletion 危险门与 predictProtectedQuickFiles 预告共用单一真相源)。
914
987
  // SillySpec 核心流程代码。W6 重构后 src/run.js(23 行 barrel)/ src/progress.js(facade)
915
988
  // 把真正逻辑下沉到 src/run/、src/progress/ 子目录——旧的精确文件名匹配