sillyspec 3.28.3 → 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 (56) hide show
  1. package/.claude/skills/sillyspec-auto/SKILL.md +54 -54
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -114
  3. package/.claude/skills/sillyspec-continue/SKILL.md +17 -17
  4. package/.claude/skills/sillyspec-execute/SKILL.md +222 -222
  5. package/.claude/skills/sillyspec-knowledge/SKILL.md +40 -40
  6. package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
  7. package/.claude/skills/sillyspec-propose/SKILL.md +16 -16
  8. package/.claude/skills/sillyspec-resume/SKILL.md +21 -21
  9. package/.claude/skills/sillyspec-state/SKILL.md +25 -25
  10. package/.claude/skills/sillyspec-workspace/SKILL.md +33 -33
  11. package/AGENTS.md +24 -0
  12. package/INSTRUCTIONS.md +12 -0
  13. package/package.json +1 -1
  14. package/src/archive-delta.js +82 -0
  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 +3 -1
  20. package/src/docs-check.js +34 -0
  21. package/src/doctor-diagnostics.js +441 -5
  22. package/src/foreign-declared.js +53 -2
  23. package/src/index.js +181 -6
  24. package/src/module-impact.js +77 -12
  25. package/src/plan-adopt-waves.js +13 -5
  26. package/src/progress/change-registry.js +16 -0
  27. package/src/progress.js +62 -1
  28. package/src/quick-recommend.js +6 -1
  29. package/src/quicklog.js +10 -2
  30. package/src/run/command.js +30 -9
  31. package/src/run/complete-handlers.js +152 -6
  32. package/src/run/complete.js +45 -0
  33. package/src/run/gates.js +94 -6
  34. package/src/run/prompt.js +96 -5
  35. package/src/run/shared.js +74 -6
  36. package/src/run/stage.js +35 -0
  37. package/src/runtime-hygiene.js +84 -0
  38. package/src/spec-sync.js +63 -8
  39. package/src/stage-contract.js +83 -3
  40. package/src/stage-review.js +3 -0
  41. package/src/stages/archive.js +4 -4
  42. package/src/stages/brainstorm.js +2 -8
  43. package/src/stages/doctor.js +41 -515
  44. package/src/stages/plan-postcheck.js +191 -7
  45. package/src/stages/plan.js +5 -26
  46. package/src/stages/quick.js +11 -11
  47. package/src/stages/verify.js +10 -6
  48. package/src/sync-noise.js +158 -0
  49. package/src/sync.js +42 -17
  50. package/src/task-review.js +25 -1
  51. package/src/verify-facts-schema.js +141 -0
  52. package/src/verify-postcheck.js +257 -19
  53. package/src/verify-probes.js +142 -13
  54. package/src/workflow.js +6 -0
  55. package/src/worktree-apply.js +51 -13
  56. package/src/worktree.js +5 -1
@@ -20,10 +20,10 @@
20
20
  * './run/stage.js' → './shared.js' 等同级
21
21
  */
22
22
  import { basename, join, resolve, dirname } from 'node:path'
23
- import { existsSync, readdirSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs'
23
+ import { existsSync, readdirSync, mkdirSync, writeFileSync, readFileSync, rmSync, unlinkSync } from 'node:fs'
24
24
  import { randomBytes, randomUUID } from 'node:crypto'
25
25
  import { writeAtomicSync } from '../fs-atomic.js'
26
- import { resolveSpecDir, countAncestorSpecDirs, ancestorSpecDirs, resolveAncestorCeiling, resolveChangeDir, triggerSync, getStageSteps, formatWaitOptions, checkApproval, warnApprovalUnknown, didYouMean, assertSafeChangeName, detectQuickSessionDrift, detectWorktreeSpecDrift, resolveRuntimeRoot, resolveQuickSessionsDir, writePlatformPointer, checkPlatformManaged, isSelfReferentialSpecRoot, PLATFORM_MANAGED_FILENAME } from './shared.js'
26
+ import { resolveSpecDir, countAncestorSpecDirs, ancestorSpecDirs, resolveAncestorCeiling, resolveChangeDir, triggerSync, getStageSteps, formatWaitOptions, checkApproval, warnApprovalUnknown, didYouMean, assertSafeChangeName, detectQuickSessionDrift, detectWorktreeSpecDrift, resolveRuntimeRoot, resolveQuickSessionsDir, writePlatformPointer, checkPlatformManaged, isSelfReferentialSpecRoot, isTempResidueSpecRoot, PLATFORM_MANAGED_FILENAME } from './shared.js'
27
27
  import { resolveQuickLinkedChanges } from './quick-audit.js'
28
28
  import { outputStep, collectStageWaitHistory } from './prompt.js'
29
29
  import { completeStep, skipStep, waitStep, continueStep } from './complete.js'
@@ -100,11 +100,18 @@ export function looksLikeMsysMangledPath(v) {
100
100
  /**
101
101
  * MSYS 污染告警出口:命中嗅探时向 stderr 打 flag 名 + 值前缀 + 修复指引(不阻断)。
102
102
  */
103
- function warnMsysMangledFlag(flag, value) {
103
+ function warnMsysMangledFlag(flag, value, { block = false } = {}) {
104
104
  if (!looksLikeMsysMangledPath(value)) return
105
- console.error(`⚠️ ${flag} 的值疑似被 Git Bash(MSYS) 路径转换污染:「${value.slice(0, 60)}${value.length > 60 ? '…' : ''}」`)
106
- console.error(' 以 / 开头的文案在 Git Bash 下会被展开成 <Git 安装目录>/… 绝对路径后才传入 CLI。')
107
- console.error(' 非本意 → 去掉前导 / 或改写表述后重发本命令;确需原样 → 命令前加 MSYS_NO_PATHCONV=1。')
105
+ const head = `⚠️ ${flag} 的值疑似被 Git Bash(MSYS) 路径转换污染:「${value.slice(0, 60)}${value.length > 60 ? '…' : ''}」`
106
+ const fix = ' 以 / 开头的文案在 Git Bash 下会被展开成 <Git 安装目录>/… 绝对路径后才传入 CLI。\n 非本意 → 去掉前导 / 或改写表述后重发本命令;确需原样 → 命令前加 MSYS_NO_PATHCONV=1。'
107
+ // 2026-09-09 ql-20260909-003(轮次经济学 §3.4):--output/--input 携带落盘语义(标题/正文
108
+ // 会写进 QUICKLOG),warn 会被忽略继续落脏标题——升 exit 2 阻断;其余调用点维持 warn。
109
+ if (block) {
110
+ console.error(head + '\n' + fix + '\n 已阻断:修复后重发本命令(进度未推进,无重跑代价)。')
111
+ process.exit(2)
112
+ }
113
+ console.error(head)
114
+ console.error(fix)
108
115
  }
109
116
 
110
117
  /**
@@ -427,6 +434,15 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
427
434
  // .sillyspec,无跨库状态分裂风险;非自指声明维持 fail-closed 原样(状态保护阻断)。
428
435
  if (isSelfReferentialSpecRoot(cwd, decl.specRoot)) {
429
436
  console.warn(`⚠️ 检测到陈旧的自指平台接管声明(repo-native junction 回环,原 specRoot 指回本地 .sillyspec),已降级并按本地模式运行;可 sillyspec platform disconnect 清理残留声明: ${join(cwd, PLATFORM_MANAGED_FILENAME)}`)
437
+ } else if (isTempResidueSpecRoot(cwd, decl.specRoot)) {
438
+ // temp 残留降级(2026-09-08 temp 投毒治理,与入口一 resolvePlatformSpecDir 同源
439
+ // 判定):声明 specRoot 在系统 temp 且 cwd 不在 temp → 可证伪的测试/联调残留。
440
+ // 与自指降级不同点:直接自动清理声明(自指场景 specRoot 指回本地 .sillyspec 有
441
+ // 真实数据面,留待 disconnect 是保守正确;temp 场景声明描述的 specRoot 物理上
442
+ // 就是临时产物,清理无损失,且不清理则每次命令重复 warn)。不用 disconnect——
443
+ // 它会连带清 local.yaml platform 段,误伤真实平台连接。删除失败仍按本地继续。
444
+ console.warn(`⚠️ 检测到 temp 残留平台接管声明(specRoot 指向系统 temp 目录:${decl.specRoot},测试/联调投毒特征),已自动清理并按本地模式运行: ${join(cwd, PLATFORM_MANAGED_FILENAME)}`)
445
+ try { unlinkSync(join(cwd, PLATFORM_MANAGED_FILENAME)) } catch { /* best effort */ }
430
446
  } else {
431
447
  console.error(`❌ 平台接管声明生效:本项目已由平台托管(原 specRoot: ${decl.specRoot || '(未记录)'}),但恢复指针缺失,拒绝静默回退本地模式。`)
432
448
  console.error(` 声明文件: ${join(cwd, PLATFORM_MANAGED_FILENAME)}`)
@@ -537,7 +553,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
537
553
  let outputText = null
538
554
  const outputValue = getFlagValue('--output')
539
555
  if (outputValue !== null) outputText = outputValue
540
- warnMsysMangledFlag('--output', outputValue)
556
+ warnMsysMangledFlag('--output', outputValue, { block: true })
541
557
 
542
558
  // 解析 quick 末步四字段参数(2026-08-21 agent-手工产出审计第二批 F6):--req/--cause/
543
559
  // --solution/--result 各传一项,CLI 合成单行四字段 outputText(quicklog 落盘侧
@@ -580,7 +596,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
580
596
  break
581
597
  }
582
598
  }
583
- warnMsysMangledFlag('--input', inputText)
599
+ warnMsysMangledFlag('--input', inputText, { block: true })
584
600
 
585
601
  // 解析 --linked-changes <a,b|none>(quick 专用:显式声明关联变更,CI/脚本友好)
586
602
  // 与 --change 解耦:--linked-changes 语义清晰(关联变更),不与「指定变更名」混淆。
@@ -1250,7 +1266,12 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
1250
1266
  if (!qlId) {
1251
1267
  const guardPath = join(specBase, '.runtime', 'quick-sessions', cancelSessionRaw, 'guard.json')
1252
1268
  try {
1253
- if (existsSync(guardPath)) qlId = JSON.parse(readFileSync(guardPath, 'utf8')).qlId
1269
+ if (existsSync(guardPath)) {
1270
+ // 字段名 bug 修复(2026-09-09 §7-5,会话实证:guard 在场仍报「缺失/损坏」):
1271
+ // guard 写侧字段是 quicklogId(run/stage.js quickGuard),旧读侧读 .qlId 恒 undefined
1272
+ const g = JSON.parse(readFileSync(guardPath, 'utf8'))
1273
+ qlId = g.quicklogId || g.qlId
1274
+ }
1254
1275
  } catch { /* guard 损坏 → 下面按找不到报错 */ }
1255
1276
  }
1256
1277
  if (!qlId) {
@@ -26,12 +26,12 @@ import { basename, join, resolve, relative, isAbsolute } from 'node:path'
26
26
  import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, rmSync } from 'node:fs'
27
27
  import { renameSyncRetry, writeAtomicSync } from '../fs-atomic.js'
28
28
  import { gitQuiet } from '../git-helper.js'
29
- import { resolveChangeDir, resolveQuickSessionsDir, safeGit, auditQuickCompletion, triggerSync, isQuickMetadata, resolveRuntimeRoot, collectOtherQuickSessionDeclarations, mergeQuickBoundaryFiles } from './shared.js'
29
+ import { resolveChangeDir, resolveQuickSessionsDir, safeGit, auditQuickCompletion, triggerSync, isQuickMetadata, isQuicklogFileLineNoise, resolveRuntimeRoot, collectOtherQuickSessionDeclarations, mergeQuickBoundaryFiles } from './shared.js'
30
30
  import { detectConcurrentChanges, formatConcurrentWarning, resolveConcurrentAnchor } from './concurrent-detect.js'
31
31
  import { stageRegistry } from '../stages/index.js'
32
32
  import { SCAN_STATUS, POINTER_STATUS } from '../constants.js'
33
33
  import { printQuickAuditReview, runQuickTestLintGate, printQuickTestLintGate } from './quick-audit.js'
34
- import { validateQuickResult, allocateQuicklogEntry, appendQuicklogEntryWithId, findQuicklogEntry, completeQuicklogEntry, extractTitleFromResult } from '../quicklog.js'
34
+ import { validateQuickResult, allocateQuicklogEntry, appendQuicklogEntryWithId, findQuicklogEntry, completeQuicklogEntry, extractTitleFromResult, parseFileNotes, getQuickFileNotes } from '../quicklog.js'
35
35
  import { getRule } from '../stage-contract-spec.js'
36
36
  import { archiveDestDirName } from '../stage-contract.js'
37
37
 
@@ -121,11 +121,115 @@ export function findAlreadyArchivedDir(archiveDir, changeName) {
121
121
  return null
122
122
  }
123
123
 
124
+ /**
125
+ * reviewedFiles[0] → 归属变更名。契约为 `changes/<change>/<doc>`(stage-review.js
126
+ * getLatestStageReviewRunId)。取 `changes/` 后第一路径段精确相等——`includes('changes/'+name+'/')`
127
+ * 会让短名 login 命中 2026-08-01-login(坑 marker-suffix-overmatch 同类)。
128
+ * @returns {string|null}
129
+ */
130
+ function ownerFromReviewedFiles(reviewedFiles) {
131
+ const r0 = Array.isArray(reviewedFiles) ? String(reviewedFiles[0] || '') : ''
132
+ const m = r0.replace(/\\/g, '/').match(/(?:^|\/)changes\/([^/]+)\//)
133
+ return m ? m[1] : null
134
+ }
135
+
136
+ /**
137
+ * 归档后按 change 精确回收 .runtime 取证(ql-20260908-005-7549)。
138
+ *
139
+ * 调用时点:handleArchiveConfirmStep 已把 reconcile/apply-pathspec 吃进 delta.md 之后
140
+ * (fail-soft 生成失败也不阻断——变更已终态,runtime 取证无读者)。自愈归档 / quick 轻量
141
+ * 归档 / change-delete 无 delta 同样可删。
142
+ *
143
+ * 归属 fail-closed(禁 mtime 猜、禁后缀匹配):
144
+ * apply-pathspec-<change>.txt 精确文件名
145
+ * execute-runs/<runId>/ 仅当 `change` 戳全等(无戳旧 run 留下,不按覆盖度启发式删)
146
+ * stage-reviews/<dir>/ 仅当 review.json reviewedFiles 首段 === changeName
147
+ * verify-runs/<ts>/ 仅当目录内 JSON 的 change 字段集合 size=1 且等于本变更
148
+ * 本轮不扩 endpoint-baselines / contract-artifacts / last-delta.json。
149
+ *
150
+ * fail-open:卫生动作失败不抛,返回 { ok:true }。
151
+ *
152
+ * @param {string} runtimeRoot
153
+ * @param {string} changeName
154
+ * @returns {{ ok: true, removed: number }}
155
+ */
156
+ export function pruneArchivedChangeRuntime(runtimeRoot, changeName) {
157
+ if (!runtimeRoot || !changeName || !existsSync(runtimeRoot)) return { ok: true, removed: 0 }
158
+ let removed = 0
159
+ const gone = (p, recursive = false) => {
160
+ try {
161
+ if (recursive) rmSync(p, { recursive: true, force: true })
162
+ else unlinkSync(p)
163
+ removed++
164
+ } catch { /* 单份失败不连坐,下次归档/删变更再收 */ }
165
+ }
166
+
167
+ try {
168
+ const pathspec = join(runtimeRoot, `apply-pathspec-${changeName}.txt`)
169
+ if (existsSync(pathspec)) gone(pathspec)
170
+ } catch {}
171
+
172
+ try {
173
+ const execDir = join(runtimeRoot, 'execute-runs')
174
+ if (existsSync(execDir)) {
175
+ for (const runId of readdirSync(execDir)) {
176
+ const stampFile = join(execDir, runId, 'change')
177
+ if (!existsSync(stampFile)) continue
178
+ let stamp = ''
179
+ try { stamp = readFileSync(stampFile, 'utf8').trim() } catch { continue }
180
+ if (stamp !== changeName) continue
181
+ gone(join(execDir, runId), true)
182
+ }
183
+ }
184
+ } catch {}
185
+
186
+ try {
187
+ const srDir = join(runtimeRoot, 'stage-reviews')
188
+ if (existsSync(srDir)) {
189
+ for (const entry of readdirSync(srDir, { withFileTypes: true })) {
190
+ if (!entry.isDirectory()) continue
191
+ const rjPath = join(srDir, entry.name, 'review.json')
192
+ if (!existsSync(rjPath)) continue
193
+ let owner = null
194
+ try {
195
+ const rj = JSON.parse(readFileSync(rjPath, 'utf8'))
196
+ owner = ownerFromReviewedFiles(rj.reviewedFiles)
197
+ } catch { continue }
198
+ if (owner !== changeName) continue
199
+ gone(join(srDir, entry.name), true)
200
+ }
201
+ }
202
+ } catch {}
203
+
204
+ try {
205
+ const vrDir = join(runtimeRoot, 'verify-runs')
206
+ if (existsSync(vrDir)) {
207
+ for (const entry of readdirSync(vrDir, { withFileTypes: true })) {
208
+ if (!entry.isDirectory()) continue
209
+ const dir = join(vrDir, entry.name)
210
+ let files = []
211
+ try { files = readdirSync(dir).filter(f => f.endsWith('.json')) } catch { continue }
212
+ const owners = new Set()
213
+ for (const f of files) {
214
+ try {
215
+ const data = JSON.parse(readFileSync(join(dir, f), 'utf8'))
216
+ if (data && typeof data.change === 'string' && data.change) owners.add(data.change)
217
+ } catch { /* 单文件坏 JSON 跳过,不据此猜归属 */ }
218
+ }
219
+ if (owners.size === 1 && owners.has(changeName)) gone(dir, true)
220
+ }
221
+ }
222
+ } catch {}
223
+
224
+ return { ok: true, removed }
225
+ }
226
+
124
227
  /**
125
228
  * 归档时清理可能残留的 worktree(execute 自动清理未走到 / 有未 apply 变更被遗弃)。
126
229
  * 安全策略:有未 apply 变更时保留 worktree 并警告,避免误删用户未应用的代码。
127
230
  * 同时清理该变更的 execute / stage-review runId marker(只写不删的累积物,
128
- * 见 stage.js execute 启动固定 executeRunId / stage-review.js stageReviewMarkerPath)。
231
+ * 见 stage.js execute 启动固定 executeRunId / stage-review.js stageReviewMarkerPath)
232
+ * 以及归档后无读者的 runtime 取证(pruneArchivedChangeRuntime)。
129
233
  * 从 archiveChangeDirectory 抽出,供正常归档 + 自愈归档复用。
130
234
  */
131
235
  export async function archiveWorktreeCleanup(cwd, archiveChangeName, specBase, platformOpts = {}) {
@@ -151,6 +255,12 @@ export async function archiveWorktreeCleanup(cwd, archiveChangeName, specBase, p
151
255
  if (markers.length > 0) {
152
256
  console.log(`🧹 归档清理 ${markers.length} 个 runId marker(execute/stage-review)`)
153
257
  }
258
+ // 取证回收必须在下方 worktree 块的 `if (!meta) return` 之前:无 meta 早退是常态
259
+ // (apply 已 cleanup),漏接就会让 execute-runs/verify-runs 继续按变更数累积。
260
+ const pruned = pruneArchivedChangeRuntime(runtimeRoot, archiveChangeName)
261
+ if (pruned.removed > 0) {
262
+ console.log(`🧹 归档回收 ${pruned.removed} 项 runtime 取证(execute-runs/stage-reviews/verify-runs/apply-pathspec,按 change 精确匹配)`)
263
+ }
154
264
  }
155
265
  } catch (e) {
156
266
  console.warn(`⚠️ 归档 runId marker 清理失败(不阻断归档): ${e.message}`)
@@ -900,6 +1010,21 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
900
1010
  console.log(`🛡️ quick 边界已追加: ${added.length} 个文件(累计 ${guard.allowedFiles.length} 个): ${added.join(', ')}`)
901
1011
  }
902
1012
  }
1013
+ // --file-notes 路径同源并入边界(2026-09-08 用户反馈①:step3 只带 --file-notes 未带
1014
+ // --files 时,QUICKLOG 文件行写进了这些文件、审计却按「超出 allowedFiles + 未声明」
1015
+ // 拦——两条声明通道口径打架)。--file-notes 是显式的改动文件声明(路径部分参与落盘
1016
+ // 文件行),与 --files 同语义追加;括注只是展示不参与。读取用 getQuickFileNotes(与
1017
+ // 下方 completeQuicklogEntry 消费同一 per-process 值,读不清)。
1018
+ {
1019
+ const fileNotePaths = parseFileNotes(getQuickFileNotes()).map(n => n.path).filter(Boolean)
1020
+ if (guard && fileNotePaths.length > 0) {
1021
+ const { added } = mergeQuickBoundaryFiles(guard, fileNotePaths, cwd)
1022
+ if (added.length > 0) {
1023
+ try { writeAtomicSync(sessionGuardFile, JSON.stringify(guard, null, 2)) } catch { /* 同上:内存合并值兜底 */ }
1024
+ console.log(`🛡️ quick 边界已追加(--file-notes 声明与文件行同源): ${added.length} 个文件(累计 ${guard.allowedFiles.length} 个): ${added.join(', ')}`)
1025
+ }
1026
+ }
1027
+ }
903
1028
  // 审计:仅在有 guard 时跑(brownfield 无 guard 跳过,兼容 D-003 brownfield 行为)。
904
1029
  // task-02:mergedGuard 提升到 if 外声明,供下方并发预检钩子复用与 auditQuickCompletion
905
1030
  // 同源的 guard 字段(baselineFiles/linkedChanges)。brownfield 无 guard 时保持 null,
@@ -1068,14 +1193,16 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
1068
1193
  // 翻状态进行中→已完成 + 追加结果 + 勾选关联 tasks.md
1069
1194
  // resultText 不再截断:结构化结果块(需求/根因/方案/结果)完整落盘,多行写成字段化块。
1070
1195
  try {
1071
- // 回填实际改动文件:review.changedFiles 含 quick 自身元数据(quicklog/.runtime/modules 等),
1072
- // 用 isQuickMetadata 过滤掉,只留真实业务文件。brownfield 无 review → 空数组,文件行不动。
1196
+ // 回填实际改动文件:review.changedFiles 含 quick 自身元数据(quicklog/.runtime 等),
1197
+ // 过滤掉只留真实业务文件——但**模块卡 + changelog sidecar 保留**(isQuicklogFileLineNoise,
1198
+ // 2026-09-08 用户反馈①:收尾必改项此前每回手工补录;审计豁免面与记录面分叉)。brownfield
1199
+ // 无 review → 空数组,文件行不动。
1073
1200
  // 归属口径(2026-08-18 误归属修复):声明会话优先 attributedFiles(窗口∩声明∪同文件并发命中),
1074
1201
  // 他者窗口文件不进文件行;未声明会话/旧 review 无 attributedFiles → 兜底 changedFiles 全量。
1075
1202
  const auditFiles = Array.isArray(review?.attributedFiles)
1076
1203
  ? review.attributedFiles
1077
1204
  : (Array.isArray(review?.changedFiles) ? review.changedFiles : [])
1078
- const realFiles = auditFiles.filter(f => !isQuickMetadata(f, linkedChanges))
1205
+ const realFiles = auditFiles.filter(f => !isQuicklogFileLineNoise(f, linkedChanges))
1079
1206
  // D-8 落盘(2026-08-18 修):advisory 欠账信号从「纯打印」升级为「随条目落盘」——修复
1080
1207
  // 「欠账已记录(QUICKLOG reasons)」的不实承诺(交叉审查实证 reasons 纯 stdout,事后不可审计)。
1081
1208
  // 两周实测(2026-08-31 裁决,doc-consistency-debt §七)需要分母:信号触发次数必须可追溯。
@@ -1087,6 +1214,25 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
1087
1214
  }
1088
1215
  if (review?.docsCheckHint && review.docsCheckHint.invalid > 0) {
1089
1216
  auditNotes.push(`📎 文档引用失效:${review.docsCheckHint.invalid}/${review.docsCheckHint.total} 处 file:line 失效(sillyspec docs check 可复现)`)
1217
+ // 行号漂移自动重锚(2026-09-08 用户反馈②:活文档 file:line 硬编码随任何插入失效,
1218
+ // 每回手跑 docs check --fix 是机械活):autoReanchorDocRefs = --fix 主链路编程化封装
1219
+ // (fixable 唯一/优选命中才改 + 定点替换 + 同口径回执),只作用于本次改动的文档。
1220
+ // fail-open:重锚任何异常只退化为上方 advisory 文案,绝不阻断 --done。
1221
+ try {
1222
+ const deletedSet = new Set(review.deletedFiles || [])
1223
+ const mdChanged = (review.changedFiles || [])
1224
+ .filter(f => f.endsWith('.md') && !deletedSet.has(f) && !isQuickMetadata(f, linkedChanges))
1225
+ if (mdChanged.length > 0) {
1226
+ const { autoReanchorDocRefs, readDocsCheckConfig } = await import('../docs-check.js')
1227
+ let fixCfg = {}
1228
+ try { fixCfg = readDocsCheckConfig(cwd) || {} } catch { fixCfg = {} }
1229
+ const r = autoReanchorDocRefs(cwd, mdChanged, fixCfg)
1230
+ if (r.applied > 0) {
1231
+ auditNotes.push(`🔧 行号漂移已自动重锚 ${r.applied} 处(同口径复跑:${r.invalidBefore} → ${r.invalidAfter};剩余 ${r.remaining} 处需人工 sillyspec docs check)`)
1232
+ console.log(`🔧 文档行号漂移自动重锚:${r.applied} 处已改写,剩余 ${r.remaining} 处需人工(sillyspec docs check 可复现)`)
1233
+ }
1234
+ }
1235
+ } catch { /* 自动重锚失败退化为纯 advisory(上方 auditNote 已落盘) */ }
1090
1236
  }
1091
1237
  // 归属切分注(2026-08-18 误归属修复):窗口内未声明脏文件不进「文件:」行,但必须落盘可追溯
1092
1238
  // (多 agent 并发仓他者窗口改动 / 本会话漏声明均可能),防真实改动被静默挤走。
@@ -380,11 +380,49 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
380
380
  } else if (_cliAction === 'progressConfirm') {
381
381
  // 与 stage.js noAI 分支同语义(brainstorm/execute/verify step1 进度确认)
382
382
  executeProgressConfirm({ stageName, cwd, stageData, changeName, pm })
383
+ } else if (_cliAction === 'doctorRunDiagnostics') {
384
+ // 2026-09-09-doctor-noai:与 stage.js 分支同语义(诊断+渲染+落盘)
385
+ const { runDoctorDiagnostics, renderDoctorSummary, writeDoctorDiagnosis } = await import('../doctor-diagnostics.js')
386
+ const diag = await runDoctorDiagnostics({ cwd })
387
+ console.log(renderDoctorSummary(diag))
388
+ try { writeDoctorDiagnosis(diag, (platformOpts?.specRoot || join(cwd, '.sillyspec'))) } catch { /* 落盘 fail-soft */ }
383
389
  } else {
384
390
  throw new Error(`noAI 步骤 ${steps[currentIdx].name} 的未知 _cliAction: ${_cliAction}——请在 complete.js 注册对应分支`)
385
391
  }
386
392
  }
387
393
 
394
+ // ── 并发 --done 防护(坑 execute-concurrent-done-skips-next-wave,2026-09-08 实证)──
395
+ // 会话 B 先 --done 推进了 Wave N,会话 A 随后的 --done 会落到**新的当前步 = Wave N+1**,
396
+ // 用 Wave N 的摘要静默完成 Wave N+1(产物未落地却标完成)。CLI 短进程无法读心,两层防护:
397
+ // ① 醒目前置横幅(邻步 60s 内刚被完成 = 疑似并行会话刚推进)——操作者在看到「✅ Step N
398
+ // 完成:Wave X」与自己所做不符时可立即 --reopen,不静默;
399
+ // ② 写前重读校验(本命令读库→落盘窗口内的真实竞态):重读 DB,若本命令要完成的步骤
400
+ // 已被并行进程标 completed/skipped,拒绝覆盖式推进(防双写交错把状态机写花)。
401
+ if (stageName === 'execute' && currentIdx > 0) {
402
+ const prev = steps[currentIdx - 1]
403
+ const _prevDoneAgoSec = prev?.completedAt ? Math.round((Date.now() - new Date(prev.completedAt.replace(/(\d{4})\/(\d{2})\/(\d{2})/, '$1-$2-$3')).getTime()) / 1000) : null
404
+ if (_prevDoneAgoSec !== null && Number.isFinite(_prevDoneAgoSec) && _prevDoneAgoSec >= 0 && _prevDoneAgoSec <= 60) {
405
+ console.warn('')
406
+ console.warn(`⚠️ 并发防护:上一步「${prev.name}」${_prevDoneAgoSec}s 前刚被(可能另一会话的)--done 完成——本次将完成的是「${steps[currentIdx].name}」。`)
407
+ console.warn(` 若与你刚做完的 Wave 不符,立即停手:sillyspec run ${stageName} --reopen --from-step ${currentIdx + 1}${changeName ? ` --change ${changeName}` : ''} 退回本步,勿继续下一 Wave。`)
408
+ console.warn('')
409
+ }
410
+ }
411
+ let _dupCompletedByPeer = false
412
+ try {
413
+ const _fresh = pm.read(cwd, changeName)
414
+ const _freshSteps = _fresh?.stages?.[stageName]?.steps
415
+ if (Array.isArray(_freshSteps) && _freshSteps[currentIdx] && _freshSteps[currentIdx].name === steps[currentIdx].name
416
+ && (_freshSteps[currentIdx].status === 'completed' || _freshSteps[currentIdx].status === 'skipped')) {
417
+ _dupCompletedByPeer = true
418
+ }
419
+ } catch { /* 重读失败(平台模式瞬态等)不阻断——横幅防护仍在 */ }
420
+ if (_dupCompletedByPeer) {
421
+ console.error(`❌ 步骤「${steps[currentIdx].name}」刚被并行会话完成(本命令读库后被推进),拒绝重复推进——防把下一步误标完成。`)
422
+ console.error(` 排查:sillyspec progress show${changeName ? ` --change ${changeName}` : ''} 确认当前步;你若也完成了同一 Wave,无需再次 --done;若要完成的是下一步,重跑 --done(此时它会落在正确的当前步)。`)
423
+ process.exit(1)
424
+ }
425
+
388
426
  steps[currentIdx].status = 'completed'
389
427
  steps[currentIdx].completedAt = new Date().toLocaleString('zh-CN',{hour12:false})
390
428
  if (outputText) {
@@ -400,6 +438,13 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
400
438
  mkdirSync(artifactBase, { recursive: true })
401
439
  const ts = new Date().toISOString().slice(0,19).replace(/[-T:]/g, '')
402
440
  writeFileSync(join(artifactBase, `${changeName || 'unknown'}-${stageName}-step${currentIdx + 1}-${ts}.txt`), outputText)
441
+ // 写入侧滚动裁剪(runtime-hygiene.js,2026-09-08 系统性回收):artifacts/ 只写不收,
442
+ // 实证本仓 534 份累积。文件名时间戳在尾部、前缀是变更名(长变更晚收尾会乱序)→ 按
443
+ // mtime 排序(write-once 本地审计文件,mtime 可靠)。fail-open 静默。
444
+ try {
445
+ const { pruneTimestampedEntries } = await import('../runtime-hygiene.js')
446
+ pruneTimestampedEntries({ dir: artifactBase, keep: 100, orderBy: 'mtime' })
447
+ } catch { /* 卫生动作 fail-open */ }
403
448
  } else {
404
449
  steps[currentIdx].output = outputText
405
450
  }
package/src/run/gates.js CHANGED
@@ -552,7 +552,33 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
552
552
  // runtimeRoot 进 context(坑 verify-receipt-runtime-split):verify 回执写入方
553
553
  // reapVerifyServices 落 resolveRuntimeRoot(platformOpts, specBase),校验器读侧不透传时
554
554
  // 平台/漂移模式恒读不到回执 → 证据门退回纯字面匹配(回执机制正是为消灭它而生)
555
- const contractResult = runValidators(stageName, cwd, changeName, { projectName, specRoot: platformOpts?.specRoot, runtimeRoot: resolveRuntimeRoot(platformOpts, specBase) })
555
+ //
556
+ // ── verify 收尾前置:facts slot-backfill + verifyStartAt(2026-09-08-ir-verify-facts FR-03)──
557
+ // 执行次序(design Phase 3):backfill(结论/证据/回执槽固化)先行 → runValidators(结论门/
558
+ // 集成证据门读得到固化值与 context.verifyStartAt)→ verify 专属块(test 实测 → tests 段二次
559
+ // 回填 → cannot_verify 硬门)。verifyStartAt = DB execute 行 completed_at(只读;缺省 null →
560
+ // 证据核验走 R-05 design created_at fallback)。
561
+ let verifyStartAtIso = null
562
+ if (stageName === 'verify') {
563
+ try {
564
+ verifyStartAtIso = pm.getStageCompletedAt(cwd, changeName, 'execute')
565
+ } catch { verifyStartAtIso = null }
566
+ try {
567
+ const verifyMdPath = join(specBase, 'changes', changeName, 'verify-result.md')
568
+ if (existsSync(verifyMdPath)) {
569
+ const verifyMd = readFileSync(verifyMdPath, 'utf8')
570
+ const { backfillFactsFromMdAndTests } = await import('../verify-probes.js')
571
+ const { extractVerifyConclusionSlot } = await import('../stage-contract.js')
572
+ backfillFactsFromMdAndTests(join(specBase, 'changes', changeName, 'verify-facts.json'), {
573
+ verifyMd,
574
+ conclusion: extractVerifyConclusionSlot(verifyMd),
575
+ })
576
+ }
577
+ } catch (e) {
578
+ console.warn(`⚠️ facts slot-backfill 失败(fail-soft,不阻断;门禁仍读 md 槽): ${e && e.message ? e.message : e}`)
579
+ }
580
+ }
581
+ const contractResult = runValidators(stageName, cwd, changeName, { projectName, specRoot: platformOpts?.specRoot, runtimeRoot: resolveRuntimeRoot(platformOpts, specBase), ...(verifyStartAtIso ? { verifyStartAt: verifyStartAtIso } : {}) })
556
582
  if (contractResult.errors.length > 0) {
557
583
  console.error(`\n❌ 阶段 ${stageName} 校验失败:`)
558
584
  for (const err of contractResult.errors) {
@@ -584,7 +610,9 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
584
610
  try {
585
611
  const { resolveApplyAllowSet } = await import('../worktree-apply.js')
586
612
  const { warnFacadeCandidateFiles } = await import('../facade-hint.js')
587
- const allowMap = resolveApplyAllowSet(cwd, changeName)
613
+ // specBase 显式传(2026-09-08 平台模式修复):changes/ 实体在 specRoot,本地目录常空——
614
+ // 旧硬编码在平台模式 allow 集恒空,facade 候选预检对空集全量误报
615
+ const allowMap = resolveApplyAllowSet(cwd, changeName, { specBase })
588
616
  warnFacadeCandidateFiles({ cwd, changeName, allowSet: allowMap.get('main') })
589
617
  } catch { /* 预检失败不阻断 plan 完成 */ }
590
618
  }
@@ -598,6 +626,21 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
598
626
  console.log(`\n⏳ Verify 测试对账:CLI 亲自执行 local.yaml 的 commands.test(同步,耗时可能较长,请等待…)`)
599
627
  const testCheck = runVerifyTestCheck({ cwd, specBase, changeName, ctx })
600
628
  printVerifyTestCheck(testCheck)
629
+ // tests 段二次回填(2026-09-08-ir-verify-facts FR-03 次序:实测在 runValidators 之后,
630
+ // tests 快照此刻才可得;不参与门禁——供 P3d 追溯)
631
+ try {
632
+ const verifyMdPath2 = join(specBase, 'changes', changeName, 'verify-result.md')
633
+ if (existsSync(verifyMdPath2)) {
634
+ const { backfillFactsFromMdAndTests } = await import('../verify-probes.js')
635
+ const { extractVerifyConclusionSlot } = await import('../stage-contract.js')
636
+ const verifyMd2 = readFileSync(verifyMdPath2, 'utf8')
637
+ backfillFactsFromMdAndTests(join(specBase, 'changes', changeName, 'verify-facts.json'), {
638
+ verifyMd: verifyMd2,
639
+ conclusion: extractVerifyConclusionSlot(verifyMd2),
640
+ testCheckResult: testCheck,
641
+ })
642
+ }
643
+ } catch { /* fail-soft:回填失败不影响门禁 */ }
601
644
  if (testCheck.status === 'failed') {
602
645
  console.error('\n❌ verify 阶段被阻断:verify-result.md 自报告通过,但 CLI 实测测试失败。')
603
646
  // 并行 WIP 归因鉴别(坑 verify-reconcile-foreign-wip):实测跑在主仓共享工作区,并行会话
@@ -650,6 +693,19 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
650
693
  console.log(`\n⏳ Verify lint 对账:CLI 亲自执行 local.yaml 的 commands.lint…`)
651
694
  }
652
695
  printVerifyLintCheck(lintCheck)
696
+ // lint 硬门(2026-09-09 升硬:观察期 14 次 5 败全真阳性,pre-push 本就硬拦——fail-fast 前移;
697
+ // 逃生 SILLYSPEC_VERIFY_LINT_GATE=advisory)
698
+ const { shouldBlockVerifyLint } = await import('../verify-postcheck.js')
699
+ if (shouldBlockVerifyLint(lintCheck)) {
700
+ console.error('\n❌ verify 阶段被阻断:CLI 亲自实测 lint 失败(agent 的 lint 自报告与实测不符时以实测为准)。')
701
+ if (lintCheck.outputTail) {
702
+ const tail = lintCheck.outputTail.split('\n').slice(-8).join('\n')
703
+ console.error(' 输出(末尾):')
704
+ for (const line of tail.split('\n')) console.error(` | ${line}`)
705
+ }
706
+ console.error(' 修复后重跑 --done(进度不丢);确认要跳过实测请设 SILLYSPEC_VERIFY_LINT_GATE=advisory(审计留痕)。')
707
+ return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
708
+ }
653
709
  // 契约 parity 对账:扫前端 API 调用 vs execute 提取的 provider endpoint artifact。
654
710
  // 接线自 contract-matrix pipeline(verifyApiParity 的 CLI 入口)。
655
711
  const { runVerifyParityCheck, printVerifyParityCheck } = await import('../verify-postcheck.js')
@@ -660,12 +716,26 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
660
716
  const { runVerifyDeletionCheck, printVerifyDeletionCheck } = await import('../verify-postcheck.js')
661
717
  const deletionCheck = runVerifyDeletionCheck({ cwd, specBase, changeName })
662
718
  printVerifyDeletionCheck(deletionCheck)
663
- // ── required-evidence 对账(advisory,不阻断):闭合 execute→verify evidence 死链 ──
664
- // execute Task Review Gate 把 cannot_verify 任务的 evidence 落盘 verify-required-evidence.json,
665
- // 本探针查每个 cannot_verify 任务是否在 verify-result.md 体现(CLI 只查提及,满足度 agent 自报告)。
719
+ // ── required-evidence 对账(2026-09-08-ir-verify-facts FR-03:v2 起 cannot_verify 不闭环阻断)──
720
+ // execute Task Review Gate 把 cannot_verify 任务的 evidence 落盘 verify-required-evidence.json。
721
+ // v2 槽优先分类核验(FR-02):blocked = missing 无豁免或 satisfied 核验不过 → 阻断 verify 完成
722
+ // (此前 advisory 只查「提及」——死链不闭环);无槽存量 md 降级 legacy 子串对账(warning 不阻断)。
666
723
  const { runVerifyRequiredEvidenceCheck, printVerifyRequiredEvidenceCheck } = await import('../verify-postcheck.js')
667
- const evidenceCheck = runVerifyRequiredEvidenceCheck({ cwd, specBase, changeName })
724
+ const evidenceCheck = runVerifyRequiredEvidenceCheck({ cwd, specBase, changeName, verifyStartAt: verifyStartAtIso })
668
725
  printVerifyRequiredEvidenceCheck(evidenceCheck)
726
+ if (evidenceCheck.status === 'blocked') {
727
+ console.error(`\n❌ verify 阶段被阻断:cannot_verify 证据账未闭环(${evidenceCheck.summary})。`)
728
+ for (const d of (evidenceCheck.detailed || [])) {
729
+ console.error(` - ${d.task} [${d.status}${d.exempt ? '/豁免' : ''}] ${d.reason}`)
730
+ for (const v of (d.verification || [])) {
731
+ if (v && v.reason) console.error(` · ${v.path || '(无路径)'} [${v.pathClass || '?'}] ${v.reason}(filesExist=${v.filesExist} mtimeOk=${v.mtimeOk} diffHit=${v.diffHit})`)
732
+ }
733
+ }
734
+ console.error(` 修复:在 verify-result.md「## 证据账(cannot_verify 任务)」槽段逐 task 一行——`)
735
+ console.error(` - task-NN: satisfied | verifiedFiles: <精确路径>(CLI 核验:代码类 存在×mtime×diff 交集;日志/文档类豁免 diff)`)
736
+ console.error(` - 确实无法验证 → 填 missing 并加(豁免:<一句话理由>)后缀;部分满足 → partial + 已核验路径`)
737
+ return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
738
+ }
669
739
  // ── target_files 声明 ↔ 实际改动 对账(task-05 / ir-stage-p3a:②ERROR 阻断 / ③WARNING 放行)──
670
740
  // 「计划落空」(task 卡声明没做)此前全盲区:review.json changedFiles 是 agent 手写不能当
671
741
  // 事实源,execute 的 scope creep 只有 symbol-impact 试图抓。reconcileTargetFiles 用 git 三源
@@ -721,6 +791,24 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
721
791
  if (probeBlocked) {
722
792
  return await rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx, cwd, changeName, platformOpts)
723
793
  }
794
+ // ── 行号漂移自动重锚(2026-09-09 §7-6:quick --done 已接,verify/archive 收尾补接)──
795
+ // 本变更 diff 中的 .md 文档经 autoReanchorDocRefs 定点重锚(fixable 唯一/优选命中才改 +
796
+ // 同口径回执),消灭「src 改动平移行号 → 下次 docs gate 拦」的人工往返。挂 verify 收尾即
797
+ // 覆盖 archive 面(漂移源是 src 改动,archive 的文档同步步不产生新 src 平移)。fail-open。
798
+ try {
799
+ const { resolveVerifyChangedFiles } = await import('../verify-postcheck.js')
800
+ const mdChangedVa = (resolveVerifyChangedFiles(cwd, changeName, null, { includeWorkingTree: true, specBase }) || [])
801
+ .filter(f => f.endsWith('.md') && !f.startsWith('.sillyspec/changes/'))
802
+ if (mdChangedVa.length > 0) {
803
+ const { autoReanchorDocRefs, readDocsCheckConfig } = await import('../docs-check.js')
804
+ let fixCfgVa = {}
805
+ try { fixCfgVa = readDocsCheckConfig(cwd) || {} } catch { fixCfgVa = {} }
806
+ const rVa = autoReanchorDocRefs(cwd, mdChangedVa, fixCfgVa)
807
+ if (rVa.applied > 0) {
808
+ console.log(`🔧 文档行号漂移自动重锚(${stageName} 收尾):${rVa.applied} 处已改写(${rVa.invalidBefore} → ${rVa.invalidAfter};剩余 ${rVa.remaining} 处需人工 sillyspec docs check)`)
809
+ }
810
+ }
811
+ } catch { /* 自动重锚失败不影响收尾(docs gate 兜底) */ }
724
812
  // ── module-impact 死信探针(blocking,债单 D-1/D-5):更新结果表 pending/待办行 → 阻断 verify ──
725
813
  // 与 archive 移动前校验(extractPendingDocSyncRows)同一口径,把死信号从 archive 提前到 verify:
726
814
  // agent 在 verify 阶段就须完成文档同步并回填 done/skipped,而非拖到归档被拦(修复 perf-remediation
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
  /**
@@ -566,6 +623,14 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
566
623
  promptText = promptText.replace(/\{LOCAL_COMMANDS\}/g, localBlock)
567
624
  }
568
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
+
569
634
  // ② {GIT_DIRTY}:工作区脏文件清单(git status --porcelain,quick 收尾步消费)
570
635
  if (promptText.includes('{GIT_DIRTY}')) {
571
636
  let dirtyBlock = ''
@@ -927,13 +992,39 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
927
992
  promptText = promptText.split('{EVIDENCE_AUTO_RECOMMENDATION}').join(eaInjected)
928
993
  }
929
994
 
930
- // 注入模块上下文(brainstorm/plan/execute 阶段,基于 Module Context Index)
931
- 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) {
932
1019
  const effectiveSpecBase = resolvePromptSpecBase(platformOpts, cwd)
933
1020
  const moduleIndex = loadModuleContextIndex(effectiveSpecBase, projectName)
934
1021
  if (moduleIndex && Object.keys(moduleIndex).length > 0) {
935
- // 尝试从 step prompt / changeName 匹配模块
936
- 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
+ }
937
1028
  const injection = buildModuleContextInjection(taskDesc, moduleIndex, effectiveSpecBase, projectName)
938
1029
  if (injection) {
939
1030
  promptText = injection + '\n' + promptText