sillyspec 3.28.3 → 3.28.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/.claude/skills/sillyspec-auto/SKILL.md +54 -54
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -114
  3. package/.claude/skills/sillyspec-continue/SKILL.md +17 -17
  4. package/.claude/skills/sillyspec-execute/SKILL.md +222 -222
  5. package/.claude/skills/sillyspec-knowledge/SKILL.md +40 -40
  6. package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
  7. package/.claude/skills/sillyspec-propose/SKILL.md +16 -16
  8. package/.claude/skills/sillyspec-resume/SKILL.md +21 -21
  9. package/.claude/skills/sillyspec-state/SKILL.md +25 -25
  10. package/.claude/skills/sillyspec-workspace/SKILL.md +33 -33
  11. package/AGENTS.md +25 -0
  12. package/INSTRUCTIONS.md +12 -0
  13. package/package.json +1 -1
  14. package/results-review-tier.md +18 -0
  15. package/src/agent-session-log.js +4 -3
  16. package/src/archive-delta.js +82 -0
  17. package/src/change-delete.js +2 -1
  18. package/src/change-list.js +18 -3
  19. package/src/change-risk-profile.js +77 -6
  20. package/src/commit-suggest.js +52 -1
  21. package/src/config-cat.js +17 -1
  22. package/src/constants.js +3 -1
  23. package/src/decision-distill.js +45 -5
  24. package/src/decisions-io.js +96 -0
  25. package/src/design-facts.js +3 -0
  26. package/src/dispatch/probe.js +31 -8
  27. package/src/docs-check.js +102 -3
  28. package/src/doctor-diagnostics.js +441 -5
  29. package/src/foreign-declared.js +53 -2
  30. package/src/index.js +606 -22
  31. package/src/mcp-server.js +144 -0
  32. package/src/module-impact.js +84 -12
  33. package/src/modules.js +46 -34
  34. package/src/plan-adopt-waves.js +13 -5
  35. package/src/progress/change-registry.js +34 -0
  36. package/src/progress.js +75 -2
  37. package/src/quick-recommend.js +6 -1
  38. package/src/quicklog.js +70 -8
  39. package/src/review-dispatch.js +377 -0
  40. package/src/run/archive-distill.js +48 -0
  41. package/src/run/command.js +48 -13
  42. package/src/run/complete-handlers.js +215 -9
  43. package/src/run/complete.js +184 -2
  44. package/src/run/gate-snapshot.js +88 -0
  45. package/src/run/gates.js +206 -58
  46. package/src/run/prompt.js +150 -14
  47. package/src/run/quick-audit.js +38 -13
  48. package/src/run/shared.js +183 -12
  49. package/src/run/stage.js +52 -2
  50. package/src/run/verify-quality-scan.js +336 -0
  51. package/src/runtime-hygiene.js +84 -0
  52. package/src/scope-audit.js +735 -0
  53. package/src/sillyhub-mcp/client.js +58 -3
  54. package/src/spec-sync.js +63 -8
  55. package/src/stage-contract.js +83 -3
  56. package/src/stage-review-checklist.js +44 -0
  57. package/src/stage-review.js +125 -4
  58. package/src/stages/archive.js +60 -167
  59. package/src/stages/brainstorm.js +7 -17
  60. package/src/stages/doctor.js +41 -515
  61. package/src/stages/execute.js +13 -10
  62. package/src/stages/plan-postcheck.js +191 -7
  63. package/src/stages/plan.js +7 -35
  64. package/src/stages/quick.js +11 -11
  65. package/src/stages/verify.js +24 -39
  66. package/src/sync-noise.js +158 -0
  67. package/src/sync.js +93 -27
  68. package/src/task-review.js +133 -17
  69. package/src/taskcard.js +46 -3
  70. package/src/validate-artifacts.js +163 -0
  71. package/src/verify-facts-schema.js +141 -0
  72. package/src/verify-postcheck.js +347 -29
  73. package/src/verify-probes.js +275 -24
  74. package/src/workflow.js +6 -0
  75. package/src/worktree-apply.js +167 -24
  76. package/src/worktree.js +5 -1
  77. package/templates/agents-instruction.md +1 -0
@@ -20,13 +20,13 @@
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, warnSelfRefPointerOnce } from './shared.js'
27
27
  import { resolveQuickLinkedChanges } from './quick-audit.js'
28
28
  import { outputStep, collectStageWaitHistory } from './prompt.js'
29
- import { completeStep, skipStep, waitStep, continueStep } from './complete.js'
29
+ import { completeStep, skipStep, waitStep, continueStep, synthesizeStepOutput } from './complete.js'
30
30
  import { runStage } from './stage.js'
31
31
  import { sanitizeDesc } from '../quicklog.js'
32
32
  import { ProgressManager } from '../progress.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
  /**
@@ -260,6 +267,13 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
260
267
  process.exit(2) // 用法错 → exit 2
261
268
  }
262
269
 
270
+ // P1-3(noai-ir-roadmap §4):--meta → 本次调用渲染 SS-META 机器块(prompt.js outputStep
271
+ // 读 SILLYSPEC_RUN_META)。CLI 是短进程:env 只在本进程生命周期内有效,不跨调用泄漏;
272
+ // auto 模式本就恒渲染(autoMeta 路径不变),--meta 对常规模式补齐同款出口。
273
+ if (flags.includes('--meta')) {
274
+ process.env.SILLYSPEC_RUN_META = '1'
275
+ }
276
+
263
277
  if (!stageRegistry[stageName] && stageName !== 'auto') {
264
278
  console.error(`❌ 未知阶段: ${stageName}`)
265
279
  console.error(`可选: ${Object.keys(stageRegistry).join(', ')}, auto`)
@@ -388,7 +402,9 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
388
402
  // 忽略恢复(specRoot/runtimeRoot/workspaceId/scanRunId 全不回填,platformOpts 保持
389
403
  // 无平台参数状态),按本地模式运行——内置 sync 走本地链路,specRoot 保持本地取值链。
390
404
  if (isSelfReferentialSpecRoot(cwd, saved.specRoot)) {
391
- console.warn(`⚠️ 检测到自指平台指针(repo-native junction 回环,specRoot 指回本地 .sillyspec),已忽略并按本地模式运行: ${platformOptsFile}`)
405
+ // 跨进程窗口降频(2026-09-10 驾驭小结第五批①):daemon junction 形态下每命令命中的重复噪音
406
+ warnSelfRefPointerOnce(cwd, saved.specRoot,
407
+ `⚠️ 检测到自指平台指针(repo-native junction 回环,specRoot 指回本地 .sillyspec),已忽略并按本地模式运行: ${platformOptsFile}(10 分钟内不重复提示)`)
392
408
  } else {
393
409
  if (saved.specRoot) platformOpts.specRoot = saved.specRoot
394
410
  if (saved.runtimeRoot) platformOpts.runtimeRoot = saved.runtimeRoot
@@ -427,6 +443,15 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
427
443
  // .sillyspec,无跨库状态分裂风险;非自指声明维持 fail-closed 原样(状态保护阻断)。
428
444
  if (isSelfReferentialSpecRoot(cwd, decl.specRoot)) {
429
445
  console.warn(`⚠️ 检测到陈旧的自指平台接管声明(repo-native junction 回环,原 specRoot 指回本地 .sillyspec),已降级并按本地模式运行;可 sillyspec platform disconnect 清理残留声明: ${join(cwd, PLATFORM_MANAGED_FILENAME)}`)
446
+ } else if (isTempResidueSpecRoot(cwd, decl.specRoot)) {
447
+ // temp 残留降级(2026-09-08 temp 投毒治理,与入口一 resolvePlatformSpecDir 同源
448
+ // 判定):声明 specRoot 在系统 temp 且 cwd 不在 temp → 可证伪的测试/联调残留。
449
+ // 与自指降级不同点:直接自动清理声明(自指场景 specRoot 指回本地 .sillyspec 有
450
+ // 真实数据面,留待 disconnect 是保守正确;temp 场景声明描述的 specRoot 物理上
451
+ // 就是临时产物,清理无损失,且不清理则每次命令重复 warn)。不用 disconnect——
452
+ // 它会连带清 local.yaml platform 段,误伤真实平台连接。删除失败仍按本地继续。
453
+ console.warn(`⚠️ 检测到 temp 残留平台接管声明(specRoot 指向系统 temp 目录:${decl.specRoot},测试/联调投毒特征),已自动清理并按本地模式运行: ${join(cwd, PLATFORM_MANAGED_FILENAME)}`)
454
+ try { unlinkSync(join(cwd, PLATFORM_MANAGED_FILENAME)) } catch { /* best effort */ }
430
455
  } else {
431
456
  console.error(`❌ 平台接管声明生效:本项目已由平台托管(原 specRoot: ${decl.specRoot || '(未记录)'}),但恢复指针缺失,拒绝静默回退本地模式。`)
432
457
  console.error(` 声明文件: ${join(cwd, PLATFORM_MANAGED_FILENAME)}`)
@@ -537,7 +562,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
537
562
  let outputText = null
538
563
  const outputValue = getFlagValue('--output')
539
564
  if (outputValue !== null) outputText = outputValue
540
- warnMsysMangledFlag('--output', outputValue)
565
+ warnMsysMangledFlag('--output', outputValue, { block: true })
541
566
 
542
567
  // 解析 quick 末步四字段参数(2026-08-21 agent-手工产出审计第二批 F6):--req/--cause/
543
568
  // --solution/--result 各传一项,CLI 合成单行四字段 outputText(quicklog 落盘侧
@@ -580,7 +605,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
580
605
  break
581
606
  }
582
607
  }
583
- warnMsysMangledFlag('--input', inputText)
608
+ warnMsysMangledFlag('--input', inputText, { block: true })
584
609
 
585
610
  // 解析 --linked-changes <a,b|none>(quick 专用:显式声明关联变更,CI/脚本友好)
586
611
  // 与 --change 解耦:--linked-changes 语义清晰(关联变更),不与「指定变更名」混淆。
@@ -804,6 +829,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
804
829
  '--deep', '--quick', '--standard', // scan profile 三档显式选择(scan-profile.js 从 argv 读;互斥见下方 PROFILE_FLAGS 检测)
805
830
  '--adopt-branch', // execute 显式收编既有 sillyspec/<change> 分支为 worktree 工作分支(坑 worktree-user-branch-conflict)
806
831
  '--diff', '--base', '--full', '--report', // scan diff(D-001:command.js 只补 flag,裸 token 解析归 index.js 子命令拦截)
832
+ '--meta', // P1-3:渲染 SS-META 机器块(布尔 flag,不吃值)
807
833
  '-h',
808
834
  ])
809
835
  for (let i = 0; i < flags.length; i++) {
@@ -1250,7 +1276,12 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
1250
1276
  if (!qlId) {
1251
1277
  const guardPath = join(specBase, '.runtime', 'quick-sessions', cancelSessionRaw, 'guard.json')
1252
1278
  try {
1253
- if (existsSync(guardPath)) qlId = JSON.parse(readFileSync(guardPath, 'utf8')).qlId
1279
+ if (existsSync(guardPath)) {
1280
+ // 字段名 bug 修复(2026-09-09 §7-5,会话实证:guard 在场仍报「缺失/损坏」):
1281
+ // guard 写侧字段是 quicklogId(run/stage.js quickGuard),旧读侧读 .qlId 恒 undefined
1282
+ const g = JSON.parse(readFileSync(guardPath, 'utf8'))
1283
+ qlId = g.quicklogId || g.qlId
1284
+ }
1254
1285
  } catch { /* guard 损坏 → 下面按找不到报错 */ }
1255
1286
  }
1256
1287
  if (!qlId) {
@@ -1717,8 +1748,12 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
1717
1748
  }
1718
1749
 
1719
1750
  if (!outputText) {
1720
- console.error('auto --done requires --output')
1721
- process.exit(2) // 用法错 → exit 2
1751
+ // P0-2(noai-ir-roadmap §3):auto --done 的 --output 同样可省略——与 completeStep 同源
1752
+ // 的事实性摘要合成(此前硬拒 exit 2 是用法守卫;SS-META 的 doneCommand 仍带 --output,
1753
+ // 照抄路径零变化,省略路径不再罚一轮)。
1754
+ const _autoStep = progress?.stages?.[currentStage]?.steps?.find((s) => s && ['pending', 'in-progress', 'blocked'].includes(s.status))
1755
+ outputText = synthesizeStepOutput({ stageName: currentStage, stepName: _autoStep?.name || currentStage, cwd })
1756
+ console.log(`🤖 auto --done 未带 --output——CLI 已按事实合成步骤摘要(语义说明下次带 --output 手写)。`)
1722
1757
  }
1723
1758
 
1724
1759
  const result = await completeStep(pm, progress, currentStage, cwd, outputText, inputText, { printNext: false, changeName, platformOpts })
@@ -26,14 +26,15 @@ 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
+ import { collectNumstatByPath } from '../scope-audit.js'
37
38
 
38
39
  /**
39
40
  * 清洗项目名:只保留 ASCII 字母/数字/横线/下划线/点,过滤中文和特殊字符。
@@ -121,11 +122,115 @@ export function findAlreadyArchivedDir(archiveDir, changeName) {
121
122
  return null
122
123
  }
123
124
 
125
+ /**
126
+ * reviewedFiles[0] → 归属变更名。契约为 `changes/<change>/<doc>`(stage-review.js
127
+ * getLatestStageReviewRunId)。取 `changes/` 后第一路径段精确相等——`includes('changes/'+name+'/')`
128
+ * 会让短名 login 命中 2026-08-01-login(坑 marker-suffix-overmatch 同类)。
129
+ * @returns {string|null}
130
+ */
131
+ function ownerFromReviewedFiles(reviewedFiles) {
132
+ const r0 = Array.isArray(reviewedFiles) ? String(reviewedFiles[0] || '') : ''
133
+ const m = r0.replace(/\\/g, '/').match(/(?:^|\/)changes\/([^/]+)\//)
134
+ return m ? m[1] : null
135
+ }
136
+
137
+ /**
138
+ * 归档后按 change 精确回收 .runtime 取证(ql-20260908-005-7549)。
139
+ *
140
+ * 调用时点:handleArchiveConfirmStep 已把 reconcile/apply-pathspec 吃进 delta.md 之后
141
+ * (fail-soft 生成失败也不阻断——变更已终态,runtime 取证无读者)。自愈归档 / quick 轻量
142
+ * 归档 / change-delete 无 delta 同样可删。
143
+ *
144
+ * 归属 fail-closed(禁 mtime 猜、禁后缀匹配):
145
+ * apply-pathspec-<change>.txt 精确文件名
146
+ * execute-runs/<runId>/ 仅当 `change` 戳全等(无戳旧 run 留下,不按覆盖度启发式删)
147
+ * stage-reviews/<dir>/ 仅当 review.json reviewedFiles 首段 === changeName
148
+ * verify-runs/<ts>/ 仅当目录内 JSON 的 change 字段集合 size=1 且等于本变更
149
+ * 本轮不扩 endpoint-baselines / contract-artifacts / last-delta.json。
150
+ *
151
+ * fail-open:卫生动作失败不抛,返回 { ok:true }。
152
+ *
153
+ * @param {string} runtimeRoot
154
+ * @param {string} changeName
155
+ * @returns {{ ok: true, removed: number }}
156
+ */
157
+ export function pruneArchivedChangeRuntime(runtimeRoot, changeName) {
158
+ if (!runtimeRoot || !changeName || !existsSync(runtimeRoot)) return { ok: true, removed: 0 }
159
+ let removed = 0
160
+ const gone = (p, recursive = false) => {
161
+ try {
162
+ if (recursive) rmSync(p, { recursive: true, force: true })
163
+ else unlinkSync(p)
164
+ removed++
165
+ } catch { /* 单份失败不连坐,下次归档/删变更再收 */ }
166
+ }
167
+
168
+ try {
169
+ const pathspec = join(runtimeRoot, `apply-pathspec-${changeName}.txt`)
170
+ if (existsSync(pathspec)) gone(pathspec)
171
+ } catch {}
172
+
173
+ try {
174
+ const execDir = join(runtimeRoot, 'execute-runs')
175
+ if (existsSync(execDir)) {
176
+ for (const runId of readdirSync(execDir)) {
177
+ const stampFile = join(execDir, runId, 'change')
178
+ if (!existsSync(stampFile)) continue
179
+ let stamp = ''
180
+ try { stamp = readFileSync(stampFile, 'utf8').trim() } catch { continue }
181
+ if (stamp !== changeName) continue
182
+ gone(join(execDir, runId), true)
183
+ }
184
+ }
185
+ } catch {}
186
+
187
+ try {
188
+ const srDir = join(runtimeRoot, 'stage-reviews')
189
+ if (existsSync(srDir)) {
190
+ for (const entry of readdirSync(srDir, { withFileTypes: true })) {
191
+ if (!entry.isDirectory()) continue
192
+ const rjPath = join(srDir, entry.name, 'review.json')
193
+ if (!existsSync(rjPath)) continue
194
+ let owner = null
195
+ try {
196
+ const rj = JSON.parse(readFileSync(rjPath, 'utf8'))
197
+ owner = ownerFromReviewedFiles(rj.reviewedFiles)
198
+ } catch { continue }
199
+ if (owner !== changeName) continue
200
+ gone(join(srDir, entry.name), true)
201
+ }
202
+ }
203
+ } catch {}
204
+
205
+ try {
206
+ const vrDir = join(runtimeRoot, 'verify-runs')
207
+ if (existsSync(vrDir)) {
208
+ for (const entry of readdirSync(vrDir, { withFileTypes: true })) {
209
+ if (!entry.isDirectory()) continue
210
+ const dir = join(vrDir, entry.name)
211
+ let files = []
212
+ try { files = readdirSync(dir).filter(f => f.endsWith('.json')) } catch { continue }
213
+ const owners = new Set()
214
+ for (const f of files) {
215
+ try {
216
+ const data = JSON.parse(readFileSync(join(dir, f), 'utf8'))
217
+ if (data && typeof data.change === 'string' && data.change) owners.add(data.change)
218
+ } catch { /* 单文件坏 JSON 跳过,不据此猜归属 */ }
219
+ }
220
+ if (owners.size === 1 && owners.has(changeName)) gone(dir, true)
221
+ }
222
+ }
223
+ } catch {}
224
+
225
+ return { ok: true, removed }
226
+ }
227
+
124
228
  /**
125
229
  * 归档时清理可能残留的 worktree(execute 自动清理未走到 / 有未 apply 变更被遗弃)。
126
230
  * 安全策略:有未 apply 变更时保留 worktree 并警告,避免误删用户未应用的代码。
127
231
  * 同时清理该变更的 execute / stage-review runId marker(只写不删的累积物,
128
- * 见 stage.js execute 启动固定 executeRunId / stage-review.js stageReviewMarkerPath)。
232
+ * 见 stage.js execute 启动固定 executeRunId / stage-review.js stageReviewMarkerPath)
233
+ * 以及归档后无读者的 runtime 取证(pruneArchivedChangeRuntime)。
129
234
  * 从 archiveChangeDirectory 抽出,供正常归档 + 自愈归档复用。
130
235
  */
131
236
  export async function archiveWorktreeCleanup(cwd, archiveChangeName, specBase, platformOpts = {}) {
@@ -151,6 +256,12 @@ export async function archiveWorktreeCleanup(cwd, archiveChangeName, specBase, p
151
256
  if (markers.length > 0) {
152
257
  console.log(`🧹 归档清理 ${markers.length} 个 runId marker(execute/stage-review)`)
153
258
  }
259
+ // 取证回收必须在下方 worktree 块的 `if (!meta) return` 之前:无 meta 早退是常态
260
+ // (apply 已 cleanup),漏接就会让 execute-runs/verify-runs 继续按变更数累积。
261
+ const pruned = pruneArchivedChangeRuntime(runtimeRoot, archiveChangeName)
262
+ if (pruned.removed > 0) {
263
+ console.log(`🧹 归档回收 ${pruned.removed} 项 runtime 取证(execute-runs/stage-reviews/verify-runs/apply-pathspec,按 change 精确匹配)`)
264
+ }
154
265
  }
155
266
  } catch (e) {
156
267
  console.warn(`⚠️ 归档 runId marker 清理失败(不阻断归档): ${e.message}`)
@@ -900,6 +1011,21 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
900
1011
  console.log(`🛡️ quick 边界已追加: ${added.length} 个文件(累计 ${guard.allowedFiles.length} 个): ${added.join(', ')}`)
901
1012
  }
902
1013
  }
1014
+ // --file-notes 路径同源并入边界(2026-09-08 用户反馈①:step3 只带 --file-notes 未带
1015
+ // --files 时,QUICKLOG 文件行写进了这些文件、审计却按「超出 allowedFiles + 未声明」
1016
+ // 拦——两条声明通道口径打架)。--file-notes 是显式的改动文件声明(路径部分参与落盘
1017
+ // 文件行),与 --files 同语义追加;括注只是展示不参与。读取用 getQuickFileNotes(与
1018
+ // 下方 completeQuicklogEntry 消费同一 per-process 值,读不清)。
1019
+ {
1020
+ const fileNotePaths = parseFileNotes(getQuickFileNotes()).map(n => n.path).filter(Boolean)
1021
+ if (guard && fileNotePaths.length > 0) {
1022
+ const { added } = mergeQuickBoundaryFiles(guard, fileNotePaths, cwd)
1023
+ if (added.length > 0) {
1024
+ try { writeAtomicSync(sessionGuardFile, JSON.stringify(guard, null, 2)) } catch { /* 同上:内存合并值兜底 */ }
1025
+ console.log(`🛡️ quick 边界已追加(--file-notes 声明与文件行同源): ${added.length} 个文件(累计 ${guard.allowedFiles.length} 个): ${added.join(', ')}`)
1026
+ }
1027
+ }
1028
+ }
903
1029
  // 审计:仅在有 guard 时跑(brownfield 无 guard 跳过,兼容 D-003 brownfield 行为)。
904
1030
  // task-02:mergedGuard 提升到 if 外声明,供下方并发预检钩子复用与 auditQuickCompletion
905
1031
  // 同源的 guard 字段(baselineFiles/linkedChanges)。brownfield 无 guard 时保持 null,
@@ -1068,14 +1194,24 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
1068
1194
  // 翻状态进行中→已完成 + 追加结果 + 勾选关联 tasks.md
1069
1195
  // resultText 不再截断:结构化结果块(需求/根因/方案/结果)完整落盘,多行写成字段化块。
1070
1196
  try {
1071
- // 回填实际改动文件:review.changedFiles 含 quick 自身元数据(quicklog/.runtime/modules 等),
1072
- // 用 isQuickMetadata 过滤掉,只留真实业务文件。brownfield 无 review → 空数组,文件行不动。
1197
+ // 回填实际改动文件:review.changedFiles 含 quick 自身元数据(quicklog/.runtime 等),
1198
+ // 过滤掉只留真实业务文件——但**模块卡 + changelog sidecar 保留**(isQuicklogFileLineNoise,
1199
+ // 2026-09-08 用户反馈①:收尾必改项此前每回手工补录;审计豁免面与记录面分叉)。brownfield
1200
+ // 无 review → 空数组,文件行不动。
1073
1201
  // 归属口径(2026-08-18 误归属修复):声明会话优先 attributedFiles(窗口∩声明∪同文件并发命中),
1074
1202
  // 他者窗口文件不进文件行;未声明会话/旧 review 无 attributedFiles → 兜底 changedFiles 全量。
1075
1203
  const auditFiles = Array.isArray(review?.attributedFiles)
1076
1204
  ? review.attributedFiles
1077
1205
  : (Array.isArray(review?.changedFiles) ? review.changedFiles : [])
1078
- const realFiles = auditFiles.filter(f => !isQuickMetadata(f, linkedChanges))
1206
+ const realFiles = auditFiles.filter(f => !isQuicklogFileLineNoise(f, linkedChanges))
1207
+ // 软归属(2026-09-10 用户反馈:--file-notes 括注只落声明文件,同模块测试文件漏声明时文件行
1208
+ // 不自动补齐,连续多批手工核对):auditQuickCompletion 判定的「窗口内未声明同模块测试文件」
1209
+ // → 补入文件行 bullet 带软归属括注;审计面单列 🔍 行(可追溯、错认可手工剔除),⚠️ 不进
1210
+ // attributedFiles——硬归属口径「声明即归属」不变(2026-08-18 误归属修复)。
1211
+ const softFiles = (Array.isArray(review?.softTestFiles) ? review.softTestFiles : [])
1212
+ .map(f => String(f).replace(/\\/g, '/')).filter(f => !isQuickMetadata(f, linkedChanges))
1213
+ // task-06(FR-04):文件行/审计行并上 numstat 行数——attachQuickLineCounts 见文件尾(声明提升)
1214
+ const { annotatedRealFiles, annotatedSoftFiles, fmtLineCounts } = attachQuickLineCounts({ cwd, realFiles, softFiles, review, auditFiles, qlId })
1079
1215
  // D-8 落盘(2026-08-18 修):advisory 欠账信号从「纯打印」升级为「随条目落盘」——修复
1080
1216
  // 「欠账已记录(QUICKLOG reasons)」的不实承诺(交叉审查实证 reasons 纯 stdout,事后不可审计)。
1081
1217
  // 两周实测(2026-08-31 裁决,doc-consistency-debt §七)需要分母:信号触发次数必须可追溯。
@@ -1087,20 +1223,47 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
1087
1223
  }
1088
1224
  if (review?.docsCheckHint && review.docsCheckHint.invalid > 0) {
1089
1225
  auditNotes.push(`📎 文档引用失效:${review.docsCheckHint.invalid}/${review.docsCheckHint.total} 处 file:line 失效(sillyspec docs check 可复现)`)
1226
+ // 行号漂移自动重锚(2026-09-08 用户反馈②:活文档 file:line 硬编码随任何插入失效,
1227
+ // 每回手跑 docs check --fix 是机械活):autoReanchorDocRefs = --fix 主链路编程化封装
1228
+ // (fixable 唯一/优选命中才改 + 定点替换 + 同口径回执),只作用于本次改动的文档。
1229
+ // fail-open:重锚任何异常只退化为上方 advisory 文案,绝不阻断 --done。
1230
+ try {
1231
+ const deletedSet = new Set(review.deletedFiles || [])
1232
+ const mdChanged = (review.changedFiles || [])
1233
+ .filter(f => f.endsWith('.md') && !deletedSet.has(f) && !isQuickMetadata(f, linkedChanges))
1234
+ if (mdChanged.length > 0) {
1235
+ const { autoReanchorDocRefs, readDocsCheckConfig } = await import('../docs-check.js')
1236
+ let fixCfg = {}
1237
+ try { fixCfg = readDocsCheckConfig(cwd) || {} } catch { fixCfg = {} }
1238
+ const r = autoReanchorDocRefs(cwd, mdChanged, fixCfg)
1239
+ if (r.applied > 0) {
1240
+ auditNotes.push(`🔧 行号漂移已自动重锚 ${r.applied} 处(同口径复跑:${r.invalidBefore} → ${r.invalidAfter};剩余 ${r.remaining} 处需人工 sillyspec docs check)`)
1241
+ console.log(`🔧 文档行号漂移自动重锚:${r.applied} 处已改写,剩余 ${r.remaining} 处需人工(sillyspec docs check 可复现)`)
1242
+ }
1243
+ }
1244
+ } catch { /* 自动重锚失败退化为纯 advisory(上方 auditNote 已落盘) */ }
1090
1245
  }
1091
1246
  // 归属切分注(2026-08-18 误归属修复):窗口内未声明脏文件不进「文件:」行,但必须落盘可追溯
1092
1247
  // (多 agent 并发仓他者窗口改动 / 本会话漏声明均可能),防真实改动被静默挤走。
1248
+ // 软归属拆分(2026-09-10):softFiles 中的同模块测试文件已补入文件行,不再占 ⚖️「未计入」
1249
+ // 文案(否则审计行与文件行自相矛盾),单列 🔍 行交代软归属依据与剔除指引。
1093
1250
  if (Array.isArray(review?.undeclaredFiles) && review.undeclaredFiles.length > 0) {
1094
- const undeclared = review.undeclaredFiles.filter(f => !isQuickMetadata(f, linkedChanges))
1251
+ const softSet = new Set(softFiles)
1252
+ const undeclared = review.undeclaredFiles.filter(f => !isQuickMetadata(f, linkedChanges) && !softSet.has(String(f).replace(/\\/g, '/')))
1095
1253
  if (undeclared.length > 0) {
1096
- auditNotes.push(`⚖️ 归属切分:${undeclared.length} 个窗口内未声明脏文件未计入文件行(并行会话改动或本会话漏声明):${undeclared.join(', ')}`)
1254
+ auditNotes.push(`⚖️ 归属切分:${undeclared.length} 个窗口内未声明脏文件未计入文件行(并行会话改动或本会话漏声明):${undeclared.map(f => `${f}${fmtLineCounts(f)}`).join(', ')}`)
1255
+ }
1256
+ if (softFiles.length > 0) {
1257
+ auditNotes.push(`🔍 软归属:${softFiles.length} 个窗口内未声明同模块测试文件已补入文件行(若属并行会话改动请手工剔除):${annotatedSoftFiles.join(', ')}`)
1258
+ console.log(`🔍 软归属:${softFiles.length} 个窗口内未声明同模块测试文件已按软归属补入文件行(若属并行会话改动请手工剔除):${annotatedSoftFiles.join(', ')}`)
1097
1259
  }
1098
1260
  }
1099
1261
  await completeQuicklogEntry(specBase, gitUser, qlId, {
1100
1262
  resultText: outputText || '',
1101
1263
  linkedChanges,
1102
- changedFiles: realFiles,
1264
+ changedFiles: annotatedRealFiles,
1103
1265
  auditNotes,
1266
+ softFiles,
1104
1267
  })
1105
1268
  console.log(`📝 QUICKLOG 条目 ${qlId} 已标记完成`)
1106
1269
  // 刷新 DB title:从 step3「需求:」提取(agent 可改 title 的途径),覆盖启动时的兜底快照。
@@ -1682,3 +1845,46 @@ export async function handleScanStageCompleted({ stageName, currentIdx, cwd, pro
1682
1845
  return null
1683
1846
  }
1684
1847
 
1848
+ // ── task-06(2026-09-10-change-scope-audit,FR-04):quick 文件行/审计行 numstat 行数注入 ──
1849
+ // collectNumstatByPath 与 scope-audit 命令同源采集(D-003 单一真相;R-03 单次 git 调用非逐文件)。
1850
+ // 基点 'HEAD' = 未提交工作区窗口(D-004:quick 无 merge-base 锚,对未提交窗口采集语义自洽;
1851
+ // untracked 新文件 wc-l 记全 + 行、binary 显 BIN)。纯展示列(advisory,D-006):采集失败/行数
1852
+ // 不可得 → 括注省略不出伪数据;不改 auditQuickCompletion 判定、runQuickTestLintGate 与任何门禁。
1853
+ // 放文件尾(函数声明提升)而不内联进 handleQuickStageCompletion:docs/platform-interface-map
1854
+ // 行号锚在上方(complete-handlers.js:1688 handleScanStageCompleted),内联大块会把 doc-ref 锚
1855
+ // 推出关键词窗口。
1856
+ //
1857
+ // 消费分工(调用方 quick 收尾):
1858
+ // - annotatedRealFiles → completeQuicklogEntry changedFiles(QUICKLOG「文件:」行/bullet 带括注;
1859
+ // fileNotes 模式下 realFiles 本就不渲染,括注只出现在回退渲染面)
1860
+ // - annotatedSoftFiles → 🔍 软归属审计行 + console;softFiles 传参保持**裸路径**——
1861
+ // flipEntryInContent 用它与 fileNotes.path 精确等值去重(带括注会不等值 → 同文件双 bullet)
1862
+ // - fmtLineCounts → ⚖️ 归属切分审计行逐文件括注(降级返回空串 → 原样不带)
1863
+ // - 已提交降级(D-004):窗口空(改动已 commit/会话未产生改动)且 QUICKLOG 条目已存在 →
1864
+ // 记录态提示读 QUICKLOG 条目文件行,不以空表冒充实时(与 scope-audit quick 模式 note 同口径)
1865
+ function attachQuickLineCounts({ cwd, realFiles, softFiles, review, auditFiles, qlId }) {
1866
+ const posix = (f) => String(f).replace(/\\/g, '/')
1867
+ const lineCountFiles = [...new Set([...(realFiles || []).map(posix), ...(softFiles || [])])]
1868
+ let numstatByPath = new Map()
1869
+ if (lineCountFiles.length > 0) {
1870
+ try {
1871
+ numstatByPath = collectNumstatByPath(cwd, lineCountFiles, { baseRef: 'HEAD' })
1872
+ } catch { /* 采集异常 → 行数列整体降级省略(fail-soft) */ }
1873
+ }
1874
+ const fmtLineCounts = (f) => {
1875
+ const st = numstatByPath.get(posix(f))
1876
+ if (!st) return ''
1877
+ if (st.kind === 'binary') return '(BIN)'
1878
+ if (!Number.isFinite(st.additions) || !Number.isFinite(st.deletions)) return ''
1879
+ return `(+${st.additions}/-${st.deletions})`
1880
+ }
1881
+ const annotatedRealFiles = (realFiles || []).map(f => `${f}${fmtLineCounts(f)}`)
1882
+ const annotatedSoftFiles = (softFiles || []).map(f => `${f}${fmtLineCounts(f)}`)
1883
+ if (lineCountFiles.length > 0) {
1884
+ console.log(`📊 变更行数(vs HEAD 未提交窗口):${[...annotatedRealFiles, ...annotatedSoftFiles].join(', ')}`)
1885
+ } else if (review && Array.isArray(auditFiles) && auditFiles.length === 0) {
1886
+ console.log(`ℹ️ 本会话无未提交改动(窗口已提交或会话未产生改动)——实时行数不可采,文件行请读 QUICKLOG 条目 ${qlId}(记录态)`)
1887
+ }
1888
+ return { annotatedRealFiles, annotatedSoftFiles, fmtLineCounts }
1889
+ }
1890
+