sillyspec 3.25.1 → 3.25.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/quicklog.js CHANGED
@@ -11,6 +11,7 @@
11
11
  * 无新 npm 依赖(仅 fs/path/crypto)——匹配项目零 FS 工具依赖的风格。
12
12
  */
13
13
  import { join, dirname, basename } from 'path'
14
+ import { getRule } from './stage-contract-spec.js'
14
15
  import {
15
16
  openSync, closeSync, unlinkSync, statSync, mkdirSync, existsSync,
16
17
  readFileSync, writeFileSync, readdirSync, renameSync, appendFileSync,
@@ -87,7 +88,8 @@ function sanitizeResult(resultText) {
87
88
  }
88
89
 
89
90
  // 结果块必填字段(quick step3 --done --output 的结构化结果模板,见 src/stages/quick.js)。
90
- const RESULT_REQUIRED_LABELS = ['需求:', '根因:', '方案:', '结果:']
91
+ // 4 个必填字段标签从 manifest 同源(stage-contract-spec.js quick.result-labels),prompt 事前契约与本校验单源。
92
+ const RESULT_REQUIRED_LABELS = getRule('quick.result-labels').data.literals
91
93
 
92
94
  /**
93
95
  * quick step3 结果摘要结构校验(确定性:只查「必填字段是否都在」,不判内容质量——
@@ -213,7 +215,7 @@ async function checkTaskCheckbox(specBase, change, qlId) {
213
215
  // 可能是 CRLF,split('\n') 后每行带 \r。原代码 `lines[i] === '状态:进行中'` 精确匹配
214
216
  // 恒失败 → 走 splice「兜底插入」→ 条目内同时出现「状态:已完成」+「状态:进行中」。
215
217
  // 状态行匹配与「已完成」判断统一用行首前缀匹配,容忍行尾 \r;写入保持 CRLF 不扩大改动。
216
- function flipEntryInContent(content, qlId, result) {
218
+ function flipEntryInContent(content, qlId, result, changedFiles = []) {
217
219
  const lines = content.split('\n')
218
220
  const startIdx = lines.findIndex(l => l.startsWith(`## ${qlId} |`))
219
221
  if (startIdx === -1) return null
@@ -226,6 +228,12 @@ function flipEntryInContent(content, qlId, result) {
226
228
  for (let i = startIdx + 1; i < endIdx; i++) {
227
229
  if (/^状态:进行中\r?$/.test(lines[i])) { lines[i] = '状态:已完成'; flipped = true }
228
230
  if (lines[i].startsWith('结果:')) hasResult = true
231
+ // 回填实际改动文件(调用方已用 isQuickMetadata 过滤 quick 自身元数据,只留业务文件)。
232
+ // 原地替换 lines[i],不改数组长度 → 与下方状态/结果 splice 的索引完全解耦。
233
+ // changedFiles 空(brownfield 无 guard / 调用方未传)→ 不动文件行,保持「(见实际改动)」。
234
+ if (changedFiles.length > 0 && lines[i].startsWith('文件:')) {
235
+ lines[i] = `文件:${changedFiles.join(', ')}`
236
+ }
229
237
  }
230
238
  if (!flipped && !lines.slice(startIdx, endIdx).some(l => /^状态:已完成\r?$/.test(l))) {
231
239
  // 无状态行(异常),插一条已完成
@@ -295,11 +303,13 @@ export async function allocateQuicklogEntry(specBase, gitUser, { description, li
295
303
  /**
296
304
  * 翻某 qlId 条目为「已完成」+ 追加结果 + 勾选关联 tasks.md。持锁。
297
305
  */
298
- export async function completeQuicklogEntry(specBase, gitUser, qlId, { resultText = '', linkedChanges = [] } = {}) {
306
+ export async function completeQuicklogEntry(specBase, gitUser, qlId, { resultText = '', linkedChanges = [], changedFiles = [] } = {}) {
299
307
  const quicklogDir = join(specBase, 'quicklog')
300
308
  const lockPath = join(quicklogDir, `.QUICKLOG-${(gitUser || 'unknown')}.md.lock`)
301
309
  const result = sanitizeResult(resultText)
302
310
  const linked = Array.isArray(linkedChanges) ? linkedChanges : []
311
+ // 实际改动文件(调用方 complete-handlers 已用 isQuickMetadata 过滤 quick 自身元数据)。空 → 不动文件行。
312
+ const realFiles = Array.isArray(changedFiles) ? changedFiles.filter(Boolean) : []
303
313
 
304
314
  await withFileLock(lockPath, async () => {
305
315
  // 条目可能在主文件或轮转归档中
@@ -307,7 +317,7 @@ export async function completeQuicklogEntry(specBase, gitUser, qlId, { resultTex
307
317
  const filePath = join(quicklogDir, f)
308
318
  let content = ''
309
319
  try { content = readFileSync(filePath, 'utf8') } catch { continue }
310
- const updated = flipEntryInContent(content, qlId, result)
320
+ const updated = flipEntryInContent(content, qlId, result, realFiles)
311
321
  if (updated !== null) {
312
322
  await writeAtomic(filePath, updated) // 命中处原子落盘(只改含目标条目的那一个文件)
313
323
  break
@@ -1,38 +1,48 @@
1
- /**
2
- * run/command.js(W6 Step8c 从 run.js 抽出)。
3
- *
4
- * 命令分发层:runCommand(主入口,被 src/index.js + 6 个 test import)+ auto 模式 +
5
- * ensureStageSteps(被 step-migration test import)+ 4 个 runCommand 私有 helper
6
- * (resolveChangeName / resolveChangeNameAuto / showStatus / resetStage)。
7
- *
8
- * 调用图(单向,无环):runCommand → { runStage(stage.js), runAutoMode, resetStage, showStatus,
9
- * ensureStageSteps, completeStep/skip/wait/continue(complete.js) };runAutoMode → { outputStep,
10
- * completeStep, ensureStageSteps, checkApproval(shared) },不调 runStage(内联 outputStep)。
11
- *
12
- * 安全锚:run.js 始终 barrel。runCommand + ensureStageSteps 被 test 直接 import → run.js barrel
13
- * re-export(契约保留);其余 5 函数 runCommand 私有,无 test 直接 import。
14
- *
15
- * 路径修正(相对 src/run/):
16
- * - 动态 import './init.js' / './worktree.js' / './classify-change.js' → '../'(src/ 下退一层)
17
- * - 'fs'(runCommand 内 await import('fs'))裸模块名不变
18
- * - 静态 import './progress.js' / './stage-contract.js' / './stages/*' / './fs-atomic.js' → '../'
19
- * './run/shared.js' / './run/quick-audit.js' / './run/prompt.js' / './run/complete.js' /
20
- * './run/stage.js' → './shared.js' 等同级
21
- */
22
- import { join, resolve, dirname } from 'node:path'
23
- import { existsSync, readdirSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs'
24
- import { randomBytes, randomUUID } from 'node:crypto'
25
- import { writeAtomicSync } from '../fs-atomic.js'
26
- import { resolveSpecDir, resolveChangeDir, triggerSync, getStageSteps, formatWaitOptions, checkApproval } from './shared.js'
27
- import { resolveQuickLinkedChanges } from './quick-audit.js'
28
- import { outputStep } from './prompt.js'
29
- import { completeStep, skipStep, waitStep, continueStep } from './complete.js'
30
- import { runStage } from './stage.js'
31
- import { ProgressManager } from '../progress.js'
32
- import { validateChangeExists } from '../stage-contract.js'
33
- import { stageRegistry, auxiliaryStages } from '../stages/index.js'
34
- import { definition as brainstormAutoDef } from '../stages/brainstorm-auto.js'
35
-
1
+ /**
2
+ * run/command.js(W6 Step8c 从 run.js 抽出)。
3
+ *
4
+ * 命令分发层:runCommand(主入口,被 src/index.js + 6 个 test import)+ auto 模式 +
5
+ * ensureStageSteps(被 step-migration test import)+ 4 个 runCommand 私有 helper
6
+ * (resolveChangeName / resolveChangeNameAuto / showStatus / resetStage)。
7
+ *
8
+ * 调用图(单向,无环):runCommand → { runStage(stage.js), runAutoMode, resetStage, showStatus,
9
+ * ensureStageSteps, completeStep/skip/wait/continue(complete.js) };runAutoMode → { outputStep,
10
+ * completeStep, ensureStageSteps, checkApproval(shared) },不调 runStage(内联 outputStep)。
11
+ *
12
+ * 安全锚:run.js 始终 barrel。runCommand + ensureStageSteps 被 test 直接 import → run.js barrel
13
+ * re-export(契约保留);其余 5 函数 runCommand 私有,无 test 直接 import。
14
+ *
15
+ * 路径修正(相对 src/run/):
16
+ * - 动态 import './init.js' / './worktree.js' / './classify-change.js' → '../'(src/ 下退一层)
17
+ * - 'fs'(runCommand 内 await import('fs'))裸模块名不变
18
+ * - 静态 import './progress.js' / './stage-contract.js' / './stages/*' / './fs-atomic.js' → '../'
19
+ * './run/shared.js' / './run/quick-audit.js' / './run/prompt.js' / './run/complete.js' /
20
+ * './run/stage.js' → './shared.js' 等同级
21
+ */
22
+ import { join, resolve, dirname } from 'node:path'
23
+ import { existsSync, readdirSync, mkdirSync, writeFileSync, readFileSync, rmSync } from 'node:fs'
24
+ import { randomBytes, randomUUID } from 'node:crypto'
25
+ import { writeAtomicSync } from '../fs-atomic.js'
26
+ import { resolveSpecDir, resolveChangeDir, triggerSync, getStageSteps, formatWaitOptions, checkApproval, didYouMean, assertSafeChangeName } from './shared.js'
27
+ import { resolveQuickLinkedChanges } from './quick-audit.js'
28
+ import { outputStep } from './prompt.js'
29
+ import { completeStep, skipStep, waitStep, continueStep } from './complete.js'
30
+ import { runStage } from './stage.js'
31
+ import { ProgressManager } from '../progress.js'
32
+ import { validateChangeExists } from '../stage-contract.js'
33
+ import { stageRegistry, auxiliaryStages } from '../stages/index.js'
34
+ import { definition as brainstormAutoDef } from '../stages/brainstorm-auto.js'
35
+
36
+ // F2/F4: 哪些 flag 吃下一个 token 作「值」(其余 --flag 都是布尔,不吞值)。
37
+ // 校验循环只对 VALUE_FLAGS 跳下一个 token(否则 --done 后跟 typo 会被当 --done 的值吞掉)。
38
+ // getFlagValue 也据此拒收「下一个 token 是另一个 flag 名」当值。
39
+ const VALUE_FLAGS = new Set([
40
+ '--reason', '--options', '--answer', '--confirm-mode',
41
+ '--output', '--input', '--change', '--linked-changes',
42
+ '--spec-dir', '--spec-root', '--runtime-root', '--workspace-id', '--scan-run-id',
43
+ '--files', '--from-step', '--mode', '--dir', '--confirm-mode',
44
+ ])
45
+
36
46
  /**
37
47
  * 从 progress 或变更目录推导变更名
38
48
  */
@@ -45,7 +55,7 @@ function resolveChangeName(cwd, progress, specDir = null) {
45
55
  if (entries.length === 1) return entries[0].name
46
56
  return null
47
57
  }
48
-
58
+
49
59
  /**
50
60
  * 确保阶段的 steps 已初始化到 progress
51
61
  */
@@ -86,7 +96,7 @@ export async function ensureStageSteps(progress, stageName, cwd, specDir = null)
86
96
 
87
97
  return false
88
98
  }
89
-
99
+
90
100
  /**
91
101
  * sillyspec run <stage> 主命令
92
102
  */
@@ -101,7 +111,19 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
101
111
  }
102
112
  // 解析参数
103
113
  const stageName = args[0]
104
- const flags = args.slice(1)
114
+ let flags = args.slice(1) // let:下方规范化 --k=v 会改写
115
+
116
+ // F7: 规范化 --key=value → ['--key', 'value']。许多 CLI 通用等号语法此前被整 token 当未知参数拒绝。
117
+ // 只对值类 flag 展开(见 VALUE_FLAGS);布尔 flag 不吃值,--skip-approval=x 这种无意义,原样进校验报错。
118
+ flags = flags.flatMap(tok => {
119
+ if (typeof tok === 'string' && tok.startsWith('--') && tok.includes('=')) {
120
+ const eq = tok.indexOf('=')
121
+ const key = tok.slice(0, eq)
122
+ const val = tok.slice(eq + 1)
123
+ if (VALUE_FLAGS.has(key)) return [key, val]
124
+ }
125
+ return [tok]
126
+ })
105
127
 
106
128
  if (!stageName) {
107
129
  console.error('❌ 请指定阶段,例如: sillyspec run brainstorm')
@@ -135,7 +157,12 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
135
157
  // --spec-dir 是统一参数名,--spec-root 保留为向后兼容别名
136
158
  const getFlagValue = (name) => {
137
159
  const idx = flags.indexOf(name)
138
- return idx !== -1 && flags[idx + 1] ? flags[idx + 1] : null
160
+ if (idx === -1) return null
161
+ const next = flags[idx + 1]
162
+ // F4: 下一个 token 若是另一个 flag 名(--xxx),说明本 flag 漏值,
163
+ // 不能把 flag 名当值返回(否则 --change --done → changeName="--done" 一路污染)。
164
+ if (!next || (typeof next === 'string' && next.startsWith('--'))) return null
165
+ return next
139
166
  }
140
167
  const isDone = flags.includes('--done')
141
168
  const isSkip = flags.includes('--skip')
@@ -149,6 +176,27 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
149
176
  const isContinue = flags.includes('--continue')
150
177
  const isNonInteractive = flags.includes('--non-interactive')
151
178
  const isInteractive = flags.includes('--interactive')
179
+ // F3 冲突 flag 检测:步骤/阶段动作 flag 互斥——各自定义「对当前步骤做什么」,同时给 2+ 个
180
+ // 语义矛盾。旧实现按代码里 if 顺序静默取先命中的、其余被忽略,agent 以为生效的其实没生效
181
+ // (如 --done --reset 只 reset 不 done,agent 却以为完成了步骤)。显式报冲突让 agent 选一个。
182
+ const STEP_ACTIONS = [
183
+ ['--done', isDone, '完成当前步骤'],
184
+ ['--skip', isSkip, '跳过当前步骤'],
185
+ ['--reset', isReset, '重置整个阶段(从头开始)'],
186
+ ['--reopen', isReopen, '重新打开已完成阶段进入修订(配 --from-step)'],
187
+ ['--wait', isWait, '暂停等用户决策'],
188
+ ['--continue', isContinue, '恢复等待中的步骤'],
189
+ ['--status', isStatus, '查看进度(只读)'],
190
+ ]
191
+ const activeActions = STEP_ACTIONS.filter(([, v]) => v)
192
+ if (activeActions.length >= 2) {
193
+ console.error('❌ 步骤动作参数冲突(同时只能指定一个):')
194
+ for (const [name, , desc] of activeActions) console.error(` ${name} — ${desc}`)
195
+ console.error(' 这些动作互斥,请只保留其中一个再重试。')
196
+ process.exit(2)
197
+ }
198
+ // 注:--non-interactive 与 --interactive 的冲突在 index.js 检测(--interactive 被 index.js
199
+ // 作为全局 flag 吞掉、不透传到此处,故 isInteractive 在此恒为 false)。
152
200
  const waitReason = getFlagValue('--reason')
153
201
  const waitOptions = getFlagValue('--options')
154
202
  const continueAnswer = getFlagValue('--answer')
@@ -307,6 +355,19 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
307
355
  if (explicitLinked !== null) {
308
356
  linkedChanges = explicitLinked
309
357
  }
358
+ // F6 路径穿越消毒:change 名会被 join 进 .sillyspec/changes/<name>/ 当目录名,
359
+ // 含 ../ 或路径分隔符会逃出 changes/ 写到任意位置。quick 的 --change 复用为 sessionId
360
+ // (quick-<8hex>,正则已约束) 或 linkedChanges,故 quick 的 changeName 不在此校验。
361
+ try {
362
+ if (stageName !== 'quick' && changeName) assertSafeChangeName(changeName)
363
+ for (const lc of linkedChanges) {
364
+ if (lc && lc !== 'none') assertSafeChangeName(lc, '关联变更名(--linked-changes)')
365
+ }
366
+ } catch (e) {
367
+ console.error(`❌ ${e.message}`)
368
+ console.error(` 合法变更名示例:2026-07-27-add-login(仅字母/数字/._-,不含 / \\ ..)`)
369
+ process.exit(2)
370
+ }
310
371
  // quick 会话隔离(D-001@v1 + D-003@v1 + §4.4 跨进程传递):每会话用 sessionId 作 changeName,
311
372
  // DB 分行 progress.quick-<uuid8>,避免并行 quick 会话共享单行 progress.default.quick 互相覆盖。
312
373
  // sessionId = crypto.randomUUID 前 8 hex(摒弃旧 quick-YYYYMMDD-HHMMSS 时间戳,同秒并发撞)。
@@ -376,17 +437,22 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
376
437
  '--files', '--allow-new', '--force-baseline', '--force-rescan',
377
438
  '--json', '--dir', '--help',
378
439
  '--reopen', '--from-step', '--mode',
440
+ '--deep', // F1: scan-profile.js 从 argv 读 --deep,文档化(index.js help)但此前 knownFlags 漏列→被拒
379
441
  ])
380
442
  for (let i = 0; i < flags.length; i++) {
381
443
  const f = flags[i]
382
444
  if (f.startsWith('--')) {
383
445
  if (!knownFlags.has(f)) {
446
+ // F10: flag 级 did-you-mean(此前只命令级有)
447
+ const suggestion = didYouMean(f, [...knownFlags])
384
448
  console.error(`❌ 未知参数: ${f}`)
385
- console.error(`已知参数: ${[...knownFlags].sort().join(', ')}`)
449
+ if (suggestion) console.error(` 你是想输入「${suggestion}」吗?`)
450
+ else console.error(`已知参数: ${[...knownFlags].sort().join(', ')}`)
386
451
  process.exit(2) // 用法错 → exit 2
387
452
  }
388
- // 跳过 value 参数
389
- i++
453
+ // F2: 只有吃值的 flag 才跳下一个 token。布尔 flag(--done 等)不能 i++——
454
+ // 否则会把紧跟的 typo flag 当成 --done 的「值」吞掉,既不校验也不生效(静默忽略)。
455
+ if (VALUE_FLAGS.has(f)) i++
390
456
  }
391
457
  }
392
458
 
@@ -626,7 +692,11 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
626
692
 
627
693
  // --status
628
694
  if (isStatus) {
629
- return showStatus(progress, stageName)
695
+ // D9:状态展示末尾附「下一步该跑什么」。pm 持有状态机,按 progress 实际进度算出精确命令;
696
+ // 不传则 agent 只看到一堆 step 进度条,却不知道完成后/卡住时下一条命令是什么。
697
+ let nextSuggestion = null
698
+ try { nextSuggestion = pm._getNextSuggestion(progress) } catch { /* 读建议失败不阻断状态展示 */ }
699
+ return showStatus(progress, stageName, nextSuggestion)
630
700
  }
631
701
 
632
702
  // --skip
@@ -641,7 +711,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
641
711
 
642
712
  // --continue: 从 waiting 恢复
643
713
  if (isContinue) {
644
- return await continueStep(pm, progress, stageName, cwd, continueAnswer, { changeName: effectiveChange, nonInteractive: isNonInteractive && !isInteractive, platformOpts })
714
+ return await continueStep(pm, progress, stageName, cwd, continueAnswer, { changeName: effectiveChange, nonInteractive: isNonInteractive && !isInteractive, platformOpts, fromStep: fromStepValue })
645
715
  }
646
716
 
647
717
  // --done
@@ -653,7 +723,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
653
723
  // 默认:输出当前步骤
654
724
  return await runStage(pm, progress, stageName, cwd, effectiveChange, isSkipApproval, platformOpts, { quickFiles, isAllowNew, isForceBaseline, isForceRescan, linkedChanges, taskDescription: inputText })
655
725
  }
656
-
726
+
657
727
  /**
658
728
  * 自动推导变更名(不依赖 progress)
659
729
  */
@@ -665,8 +735,8 @@ function resolveChangeNameAuto(cwd, specDir = null) {
665
735
  if (entries.length === 1) return entries[0].name
666
736
  return null
667
737
  }
668
-
669
- function showStatus(progress, stageName) {
738
+
739
+ function showStatus(progress, stageName, nextSuggestion = null) {
670
740
  const stageData = progress.stages[stageName]
671
741
  const stageDef = stageRegistry[stageName]
672
742
 
@@ -733,8 +803,15 @@ function showStatus(progress, stageName) {
733
803
  if (step.waitedAt) console.log(` 等待时间:${step.waitedAt}`)
734
804
  }
735
805
  })
806
+
807
+ // D9:本阶段进度之后,给出全局「下一步该跑什么」——
808
+ // 否则 agent 看完进度条仍要自己推状态机(尤其阶段已完成 / 失效 / 修订中这类非线性状态)。
809
+ if (nextSuggestion && nextSuggestion.command) {
810
+ console.log(`\n👉 ${nextSuggestion.text}`)
811
+ console.log(` 下一步命令:${nextSuggestion.command}`)
812
+ }
736
813
  }
737
-
814
+
738
815
  async function resetStage(pm, progress, stageName, cwd, changeName, platformOpts = {}) {
739
816
  // execute 阶段 reset 时清理自建 worktree,否则下次 run execute 会因 existingMeta 存在
740
817
  // 直接复用带脏状态的旧 worktree(启动逻辑:meta 存在即复用,不查健康状态)
@@ -770,7 +847,7 @@ async function resetStage(pm, progress, stageName, cwd, changeName, platformOpts
770
847
  triggerSync(cwd, changeName, platformOpts)
771
848
  console.log(`🔄 ${stageName} 阶段已重置`)
772
849
  }
773
-
850
+
774
851
  /**
775
852
  * auto 模式:自动推进 brainstorm → plan → execute → verify
776
853
  */
@@ -1008,4 +1085,4 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
1008
1085
  await outputStep(next, firstPending, nextSteps, cwd, changeName, progress.project || null, platformOpts)
1009
1086
  }
1010
1087
  }
1011
-
1088
+
@@ -23,11 +23,12 @@
23
23
  import { basename, join, resolve, relative, isAbsolute } from 'node:path'
24
24
  import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, rmSync } from 'node:fs'
25
25
  import { renameSyncRetry, writeAtomicSync } from '../fs-atomic.js'
26
- import { resolveChangeDir, safeGit, auditQuickCompletion, triggerSync } from './shared.js'
26
+ import { resolveChangeDir, safeGit, auditQuickCompletion, triggerSync, isQuickMetadata } from './shared.js'
27
27
  import { stageRegistry } from '../stages/index.js'
28
28
  import { SCAN_STATUS, POINTER_STATUS } from '../constants.js'
29
29
  import { printQuickAuditReview } from './quick-audit.js'
30
30
  import { validateQuickResult, allocateQuicklogEntry, findQuicklogEntry, completeQuicklogEntry } from '../quicklog.js'
31
+ import { getRule } from '../stage-contract-spec.js'
31
32
 
32
33
  /**
33
34
  * 清洗项目名:只保留 ASCII 字母/数字/横线/下划线/点,过滤中文和特殊字符。
@@ -558,6 +559,9 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
558
559
  const gitUser = safeGit(cwd, ['config', 'user.name']).value || 'unknown'
559
560
  let qlId = guard?.quicklogId || null
560
561
  const linkedChanges = Array.isArray(guard?.linkedChanges) ? guard.linkedChanges : []
562
+ // 审计结果(仅 guard 存在时填充)。提到 if(guard) 外声明,供下方回填 QUICKLOG 文件行复用
563
+ // review.changedFiles;brownfield 无 guard 时保持 null → 文件行不回填(降级,不报错)。
564
+ let review = null
561
565
 
562
566
  // 审计:仅在有 guard 时跑(brownfield 无 guard 跳过,兼容 D-003 brownfield 行为)。
563
567
  if (guard) {
@@ -569,7 +573,7 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
569
573
  forceBaseline: guard.forceBaseline || isForceBaseline,
570
574
  allowNew: guard.allowNew || isAllowNew,
571
575
  }
572
- const review = await auditQuickCompletion(cwd, mergedGuard, { isConfirm: confirm })
576
+ review = await auditQuickCompletion(cwd, mergedGuard, { isConfirm: confirm })
573
577
  printQuickAuditReview(review)
574
578
  if (review.status === 'blocked') {
575
579
  steps[currentIdx].status = 'pending'
@@ -588,7 +592,7 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
588
592
  if (outputText) {
589
593
  const resultCheck = validateQuickResult(outputText)
590
594
  if (!resultCheck.ok) {
591
- console.error(`\n❌ quick 结果摘要结构不完整:缺少字段 ${resultCheck.missing.join('、')}`)
595
+ console.error('\n' + getRule('quick.result-labels').failMessage.replaceAll('${missing}', resultCheck.missing.join('、')))
592
596
  console.error(` --output 是 QUICKLOG「结果:」归档的唯一来源,四个标签必须放在 --output 里(不是 --input)。`)
593
597
  console.error(` 补全后重跑 --done(不丢进度),直接照抄此模板:`)
594
598
  console.error(` sillyspec run quick --done --change <changeName> --output "需求:用户/任务要什么`)
@@ -631,9 +635,15 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
631
635
  // 翻状态进行中→已完成 + 追加结果 + 勾选关联 tasks.md
632
636
  // resultText 不再截断:结构化结果块(需求/根因/方案/结果)完整落盘,多行写成字段化块。
633
637
  try {
638
+ // 回填实际改动文件:review.changedFiles 含 quick 自身元数据(quicklog/.runtime/modules 等),
639
+ // 用 isQuickMetadata 过滤掉,只留真实业务文件。brownfield 无 review → 空数组,文件行不动。
640
+ const realFiles = Array.isArray(review?.changedFiles)
641
+ ? review.changedFiles.filter(f => !isQuickMetadata(f, linkedChanges))
642
+ : []
634
643
  await completeQuicklogEntry(specBase, gitUser, qlId, {
635
644
  resultText: outputText || '',
636
645
  linkedChanges,
646
+ changedFiles: realFiles,
637
647
  })
638
648
  console.log(`📝 QUICKLOG 条目 ${qlId} 已标记完成`)
639
649
  } catch (e) {