sillyspec 3.32.2 → 3.32.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.
@@ -40,6 +40,7 @@ sillyspec flow start --change <变更名> --input "<动机与背景>
40
40
  改代码、写测试。治理工件(proposal/requirements/design)CLI 机器起草,你只需要:
41
41
 
42
42
  - 填 design.md 四节 AGENT 槽(做法概述/接口契约/边界并发四问/风险与死路)——每节至少一行,写「不适用:<理由>」也算答,空槽 flow done 拒收
43
+ - 变更目录若已有收割生成的 decisions.md(frontmatter `author: flow-machine-draft`),它是底稿不是终稿:槽4 作答含「放弃方案」的,收口前把每个放弃方案拆成独立 rejected 条目(问题=决策专属问句、否决理由=放弃理由、复潮条件=重提前提),风险与对策留在主条目——rejected 条目才走蒸馏防复潮通道,埋在散文里的否决理由进不了通道;无实质放弃方案则不动
43
44
  - 干活以 tasks.md 为进度源:做一件 → 勾一格(`- [ ]` → `- [x]`)→ 继续下一条,勿攒一把勾(勾选是收口哨兵的证据面——逐 task 核提交 token/review.json);`flow status --change <名>` 为自愿查看/恢复面(②阶段给下一任务指针与进度,非协议必需——D-007)
44
45
  - 交付代码用显式 pathspec 提交(`git commit -m "..." -- 文件1 文件2`;patch 冻结面=baseline..HEAD 提交面,未提交的代码不进审计件)
45
46
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sillyspec",
3
- "version": "3.32.2",
3
+ "version": "3.32.3",
4
4
  "description": "SillySpec CLI — 流程状态机,让 AI 严格按步骤来",
5
5
  "icon": "logo.jpg",
6
6
  "homepage": "https://sillyspec.ppdmq.top/",
@@ -33,6 +33,7 @@ import { join, isAbsolute, resolve } from 'path'
33
33
  import { gitQuiet } from './git-helper.js'
34
34
  import { parseRepoRegistry } from './stages/plan-postcheck.js'
35
35
  import { filterDeliverableFiles, classifyToolScaffold } from './worktree-apply.js'
36
+ import { crossWorktreePath } from './worktree-cross.js'
36
37
  import { resolveLatestExecuteRunIdWithTasks, readReview } from './task-review.js'
37
38
 
38
39
  // 与 verify-postcheck normalizeReconcilePath 同口径(本地实现防环):剥 ./ 前缀、反斜杠归一
@@ -228,6 +229,38 @@ export function collectRepoActual({ repoKey, specBase, cwd, runtimeRoot = null,
228
229
  }
229
230
  }
230
231
 
232
+ // 3b) B' 档 worktree-baseline 窗口(2026-10-09-verify-reuse-friction FR-07):跨仓
233
+ // worktree 在场时(verify 常跑在 apply 前)交付面在 worktree——meta.baseHash..工作树
234
+ // 全量窗口,多笔提交与未提交改动全覆盖;原 B 档 HEAD~1..HEAD 只看跨仓主副本最近一笔
235
+ // 提交,对 worktree 多笔交付是假信号(2026-10-09 tombstone 取证:声明 4 实测 0)。
236
+ // meta 缺失/worktree 不在场/不可读 → 回退 B 档(不回退语义)。
237
+ try {
238
+ const wtPath = crossWorktreePath(specBase, changeName, repoKey)
239
+ const metaPath = join(wtPath, 'meta.json')
240
+ if (existsSync(metaPath)) {
241
+ const meta = JSON.parse(readFileSync(metaPath, 'utf8'))
242
+ const base = meta && meta.baseHash
243
+ if (base && typeof base === 'string' && /^[0-9a-f]{7,40}$/i.test(base)) {
244
+ const union2 = new Set()
245
+ // baseline→工作树(已提交+已改)∪ 未跟踪新文件;两查任一可用即成窗
246
+ const d1 = gitQuiet(wtPath, ['diff', '--name-only', base], { timeout: GIT_TIMEOUT })
247
+ const d2 = gitQuiet(wtPath, ['ls-files', '--others', '--exclude-standard'], { timeout: GIT_TIMEOUT })
248
+ if (d1 !== null || d2 !== null) {
249
+ for (const src of [d1, d2]) {
250
+ for (const f of String(src || '').split('\n')) {
251
+ const n = normalizeRepoPath(f)
252
+ if (n && n !== 'meta.json' && !n.startsWith('.sillyspec/')) union2.add(n)
253
+ }
254
+ }
255
+ collectStatusInto(repoPath, union2)
256
+ return { repo: repoKey, repoPath,
257
+ anchor: { source: 'worktree-baseline-window', base, head: null, label: `跨仓 worktree baseline 窗口(base=${base.slice(0, 8)}…HEAD+工作树;多笔提交全覆盖)` },
258
+ files: [...union2].sort(), degradedReason: null }
259
+ }
260
+ }
261
+ }
262
+ } catch { /* meta 不可读/键缺失 → 回退 B 档 */ }
263
+
231
264
  // 4) B/C 档:diff HEAD~1..HEAD(现行 reconcile 口径)∪ status;diff 失败仅 status → C 档
232
265
  const union = new Set()
233
266
  const diffOut = gitQuiet(repoPath, ['diff', '--name-only', 'HEAD~1..HEAD'], { timeout: GIT_TIMEOUT })
@@ -10,9 +10,10 @@
10
10
  * 厚道有轻量变更缺的审计资产;机器合成勿手改。
11
11
  * ③ harvestSlot4Decision:design 槽4(风险与死路)实质作答收割合成 decisions.md——轻量变更决策
12
12
  * 产出为零的补口(死路与风险取舍正是 decisions.md 该记的内容;已有文件不覆盖)。
13
- * ④ writeCloseTraceArtifacts / buildThinSnapshotRows:双通道收尾留痕统一写
14
- * (2026-10-07-unify-close-trace)——thin flow done 与 heavy execute --done 共用一处一次
15
- * 写齐四件(沉淀资产面 + 对账快照面,sha256 双套同锚),新变更两通道留痕对称。
13
+ * ④ writeCloseTraceArtifacts / buildThinSnapshotRows:收口留痕单套写
14
+ * (2026-10-09-close-trace-single-set)——thin flow done 与 heavy execute --done 共用
15
+ * 一处只写 change.patch + change-patch.json 两件(对账面并入 scopeAudit 子对象;
16
+ * 此前 2026-10-07-unify-close-trace 的四件双轨形态收口为单套,旧名读侧兼容兜存量)。
16
17
  */
17
18
  import { existsSync, readFileSync, writeFileSync, readdirSync } from 'node:fs'
18
19
  import { join, relative } from 'node:path'
@@ -358,21 +359,21 @@ export function projectTraceFaceRows(rows) {
358
359
  }
359
360
 
360
361
  /**
361
- * 双通道收尾留痕统一写(2026-10-07-unify-close-trace,缺陷 thin-flow-done-no-scope-
362
- * audit-snapshot 修复方向③):thin(flow done)与 heavy(execute --done)此前各写各的
363
- * 留痕(沉淀资产面 change.patch/change-patch.json vs 对账快照面 scope-audit.patch/
364
- * scope-audit.json),消费方各认一份致每类变更恰好一张卡失真——本函数一处写齐四件,
365
- * sha256/patchStatus 双套同锚(同一 patchText 一次计算:change.patch ≡ scope-audit.patch
366
- * 字节一致、两 json 同 hash,两套留痕不可能漂移)。patchText 空/null → 双套同标 failed、
367
- * 不落 patch 文件(heavy 旧「空串当 ok」形态收口)。读侧兼容链(快照 > change-patch
368
- * 回放 > 实时区间,2026-10-07-scope-audit-thin-patch-replay)不动——本函数让新变更两套
369
- * 齐备,回退退化为存量归档兜底。
362
+ * 收口留痕单套写(2026-10-09-close-trace-single-set):thin(flow done)与 heavy
363
+ * (execute --done)统一只落 change.patch + change-patch.json 两件。原对账快照面
364
+ * (scope-audit.json + scope-audit.patch,2026-10-07-unify-close-trace 起与沉淀资产面
365
+ * 双轨并写)停写:对账数据并入 change-patch.json 的 scopeAudit 子对象,顶级键
366
+ * (change/baseline/head/files/totals/savedAt/note/moduleScope 等)原位不动——
367
+ * fr-index/knowledge-graph/flow 漂移检测与平台 assets.py/parser.py 等顶级字段读方零改动。
368
+ * patchText 一次 sha256 锚定 patch 与 patchStatus;patchText 空/null → failed、不落
369
+ * patch 文件。读侧兼容链(scopeAudit 子对象 > 旧 scope-audit.json > .runtime 快照 >
370
+ * thin 回放 > 实时区间)兜存量归档,旧名只读不写。
370
371
  *
371
372
  * @param {string} changeDir 变更目录(活跃或归档侧——归档竞态由调用方解析)
372
- * @param {object} snapObj scope-audit.json 主体(mode/ok/degradedReason/baseAnchor/totals/
373
- * rows/excluded/note[/repos];closedBy 由调用方携带标收尾通道——读侧回放按它标注,
374
- * 缺省 'execute --done' 兼容旧快照)
375
- * @param {object} meta change-patch.json 增量键(note/moduleScope 等,展开在 totals 后)
373
+ * @param {object} snapObj scopeAudit 子对象主体(mode/ok/degradedReason/baseAnchor/
374
+ * totals/rows/excluded/note[/repos];closedBy 由调用方携带标收尾通道——读侧回放按它
375
+ * 标注,缺省 'execute --done' 兼容旧快照)
376
+ * @param {object} meta change-patch.json 顶级增量键(note/moduleScope 等,展开在 totals 后)
376
377
  * @returns {{ patchStatus: 'ok'|'failed', patchSha256: string|null }}
377
378
  */
378
379
  export function writeCloseTraceArtifacts({ changeDir, change, baseline, head, files, metaTotals, patchText, savedAt, snapObj, meta = {} }) {
@@ -382,10 +383,7 @@ export function writeCloseTraceArtifacts({ changeDir, change, baseline, head, fi
382
383
  const patchSha256 = patchBody
383
384
  ? createHash('sha256').update(patchBody.replace(/\r\n/g, '\n'), 'utf8').digest('hex')
384
385
  : null
385
- if (patchOk) {
386
- writeFileSync(join(changeDir, 'change.patch'), patchBody)
387
- writeFileSync(join(changeDir, 'scope-audit.patch'), patchBody)
388
- }
386
+ if (patchOk) writeFileSync(join(changeDir, 'change.patch'), patchBody)
389
387
  writeFileSync(join(changeDir, 'change-patch.json'), JSON.stringify({
390
388
  change,
391
389
  baseline: baseline ?? null,
@@ -396,15 +394,10 @@ export function writeCloseTraceArtifacts({ changeDir, change, baseline, head, fi
396
394
  : { files: fileList.length, additions: 0, deletions: 0 },
397
395
  savedAt,
398
396
  ...meta,
397
+ scopeAudit: { ...snapObj },
399
398
  ...(patchSha256 ? { patchSha256 } : {}),
400
399
  patchStatus: patchOk ? 'ok' : 'failed',
401
400
  }, null, 2) + '\n')
402
- writeFileSync(join(changeDir, 'scope-audit.json'), JSON.stringify({
403
- ...snapObj,
404
- ...(patchSha256 ? { patchSha256 } : {}),
405
- patchStatus: patchOk ? 'ok' : 'failed',
406
- savedAt,
407
- }, null, 2) + '\n')
408
401
  return { patchStatus: patchOk ? 'ok' : 'failed', patchSha256 }
409
402
  }
410
403
 
package/src/flow.js CHANGED
@@ -1421,6 +1421,10 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1421
1421
  console.warn(`⚠️ --accept-dirty-gap:显式接受 ${freeze.dirtyWarned.length} 个未提交交付文件不进冻结面(缺口数随 change-patch.json acceptedDirtyGap 留痕)`)
1422
1422
  }
1423
1423
  }
1424
+ // 收口留痕四名全排除(2026-10-09-close-trace-single-set 后只写 change.patch/
1425
+ // change-patch.json 两件,但旧轮冻结件里可能仍有 scope-audit.json/.patch——
1426
+ // legacy 防护保留):上一轮留痕件作为 untracked 新文件被全文自嵌入即自引用循环
1427
+ // (旧 patch 内容混进新 patch,三评 P1 根因)。
1424
1428
  const ownFiles = [...new Set([...freeze.files, ...changeDirFiles])]
1425
1429
  .filter((f) => f && !f.endsWith('change.patch') && !f.endsWith('change-patch.json')
1426
1430
  && !f.endsWith('scope-audit.json') && !f.endsWith('scope-audit.patch'))
@@ -1437,8 +1441,7 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1437
1441
  const toPosix = (p) => String(p).replace(/\\/g, '/')
1438
1442
  // 复审 P2/P3a 清偿:工作树口径集 = 治理工件目录(含已提交后又改的槽位/勾选——工作树
1439
1443
  // diff 能捕捉提交后编辑)∪ 独占树未提交交付(dirtyAdded);其余交付面走提交区间。
1440
- // 排除项与 ownFiles 同款(change.patch/change-patch.json)——否则上一轮冻结件作为
1441
- // untracked 新文件被全文自嵌入(自引用循环:旧 patch 内容混进新 patch,三评 P1 根因)。
1444
+ // 排除项与 ownFiles 同款(四名全排,含 scope-audit.* 旧轮冻结件 legacy 防护)。
1442
1445
  const dirFilesForPatch = changeDirFiles.filter((f) => !f.endsWith('change.patch') && !f.endsWith('change-patch.json')
1443
1446
  && !f.endsWith('scope-audit.json') && !f.endsWith('scope-audit.patch'))
1444
1447
  const worktreeSet = new Set([...dirFilesForPatch, ...((freeze.dirtyAdded) || [])].map(toPosix))
@@ -1469,10 +1472,9 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1469
1472
  if (rec.lines.length > 0) for (const l of rec.lines) console.log(l)
1470
1473
  moduleScope = { modules: rec.modules, uncoveredDirs: rec.uncoveredDirs, moduleMaps: rec.moduleMaps }
1471
1474
  } catch { /* 对账 best-effort(三键空数组兜底) */ }
1472
- // 双通道收尾留痕统一(2026-10-07-unify-close-trace):共用 writeCloseTraceArtifacts
1473
- // 一次写齐四件——沉淀资产面(change.patch + change-patch.json,键结构/口径不变)+
1474
- // 对账快照面(scope-audit.json + scope-audit.patch,thin 通道新增):同点同锚
1475
- // (同一 patchText 一次 sha256),新变更两条通道留痕对称,读侧回退退化为存量兜底。
1475
+ // 收口留痕单套写(2026-10-09-close-trace-single-set):共用 writeCloseTraceArtifacts
1476
+ // 只写 change.patch + change-patch.json 两件——对账面数据并入 JSON 的 scopeAudit
1477
+ // 子对象(thin 通道同样有对账面),旧名 scope-audit.* 停写、读侧兼容兜存量归档。
1476
1478
  let planEntries = []
1477
1479
  try {
1478
1480
  const { parseFileChangeListDetailed } = await import('./change-list.js')
@@ -1508,7 +1510,7 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1508
1510
  ...(acceptDirtyGap && freeze.dirtyWarned.length > 0 ? { acceptedDirtyGap: freeze.dirtyWarned.length } : {}),
1509
1511
  },
1510
1512
  })
1511
- console.log(`📦 变更 patch 留档(双轨统一):change.patch + change-patch.json(${ownFiles.length} 文件,+${additions}/-${deletions})+ scope-audit.json + scope-audit.patch(对账快照同点冻结)${closeTrace.patchStatus === 'ok' ? ',sha256 已锚' : '——patch 采集失败已留痕'}`)
1513
+ console.log(`📦 变更 patch 留档(单套):change.patch + change-patch.json(${ownFiles.length} 文件,+${additions}/-${deletions},对账面并入 scopeAudit 子对象)${closeTrace.patchStatus === 'ok' ? ',sha256 已锚' : '——patch 采集失败已留痕'}`)
1512
1514
  // design 声明面自证(2026-09-25-platform-feedback-batch2 E):design.md 文件变更清单
1513
1515
  // 声明的交付文件是否都在冻结面——不在=承诺改了但没交付(承诺未兑现面,advisory 不
1514
1516
  // 阻断——可能是范围裁剪了但 design 没同步更新)
@@ -1684,7 +1686,7 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1684
1686
  try {
1685
1687
  const { harvestSlot4Decision } = await import('./flow-parity.js')
1686
1688
  const h = harvestSlot4Decision({ changeDir, change })
1687
- if (h.harvested) console.log(`🌱 槽4(风险与死路)收割 → decisions.md(随蒸馏链进 knowledge)`)
1689
+ if (h.harvested) console.log(`🌱 槽4(风险与死路)收割 → decisions.md(底稿——作答里的放弃方案请拆成 rejected 条目再收口;随蒸馏链进 knowledge)`)
1688
1690
  } catch { /* 收割 best-effort */ }
1689
1691
  try {
1690
1692
  const { distillIntoKnowledge } = await import('./decision-distill.js')
@@ -30,7 +30,7 @@ import { withFileLock } from './quicklog.js'
30
30
  import { resolveRuntimeRoot, resolveQuickSessionsDir } from './run/shared.js'
31
31
 
32
32
  /** 摩擦事件类型枚举(D-004):审查打回走专属 review_rejected,不与 gate_rollback 重复计 */
33
- const FRICTION_TYPES = new Set(['gate_rollback', 'verify_run_failed', 'review_rejected'])
33
+ const FRICTION_TYPES = new Set(['gate_rollback', 'verify_run_failed', 'review_rejected', 'gate_snapshot_fallback'])
34
34
 
35
35
  /** history 截尾上限(同 verify-lint-tally 的 20 条口径——台账防病态体量,不承载完整审计) */
36
36
  const FRICTION_HISTORY_CAP = 20
@@ -43,6 +43,7 @@ const FRICTION_LABELS = [
43
43
  ['gate_rollback', 'gate 回滚'],
44
44
  ['verify_run_failed', '验证失败'],
45
45
  ['review_rejected', '审查打回'],
46
+ ['gate_snapshot_fallback', '快照回退主仓'],
46
47
  ]
47
48
 
48
49
  /**
package/src/git-helper.js CHANGED
@@ -187,10 +187,32 @@ export function stageArchiveArtifacts(cwd) {
187
187
  const skipped = []
188
188
  const existing = candidates.filter(p => existsSync(p))
189
189
  if (existing.length === 0) return { staged, skipped }
190
- const r = safeGit(cwd, ['add', ...existing])
190
+ // 2026-10-09-verify-papercuts ④:排除嵌套运行时异物——归档变更目录内嵌套的 .sillyspec/
191
+ // (历史 cwd 漂移事故的遗留 db/日志)会被目录级 add 一并扫进暂存(当日实证:他变更的
192
+ // sillyspec.db 与 spec-sync-bg.log 被夹带提交)。pathspec exclude 精确拦截该形态,正常
193
+ // 归档产物(md/json/patch)零影响;检出异物在场时点名提示清理(不静默吞)。
194
+ const excludes = [
195
+ ':(exclude).sillyspec/changes/archive/*/.sillyspec',
196
+ ':(exclude).sillyspec/changes/archive/*/.sillyspec/**',
197
+ ]
198
+ let nestedJunk = []
199
+ try {
200
+ const archRoot = join(specBase, 'changes', 'archive')
201
+ for (const e of readdirSync(archRoot, { withFileTypes: true })) {
202
+ if (e.isDirectory() && existsSync(join(archRoot, e.name, '.sillyspec'))) {
203
+ nestedJunk.push(join(archRoot, e.name, '.sillyspec'))
204
+ }
205
+ }
206
+ } catch { /* 归档目录不可读 → 跳过提示 */ }
207
+ const r = safeGit(cwd, ['add', ...existing, ...excludes])
191
208
  if (r && r.error) {
192
209
  console.warn(` ⚠️ 归档产物 git add 失败(可手动处理):${String(r.error).slice(0, 120)}`)
193
210
  return { staged, skipped: existing }
194
211
  }
212
+ if (nestedJunk.length > 0) {
213
+ console.warn(` ⚠️ 检出 ${nestedJunk.length} 个归档目录内嵌套 .sillyspec/ 运行时异物(已从本次暂存排除):`)
214
+ for (const j of nestedJunk.slice(0, 5)) console.warn(` - ${j}`)
215
+ console.warn(` 处置:确认无归属后删除目录;若已被 git 跟踪先 git rm -r --cached <路径>(不该入库的 db/日志)`)
216
+ }
195
217
  return { staged: existing, skipped }
196
218
  }
package/src/index.js CHANGED
@@ -1334,7 +1334,7 @@ task done 四合一(r5l 方案1):review write(落 review.json+自动勾
1334
1334
  process.exit(2);
1335
1335
  }
1336
1336
  assertSafeChangeName(vpChange, '--change 变更名');
1337
- const { runVerifyProbes, renderVerifyProbesReport, generateVerifyResultSkeleton, resolveVerifyProbesSpecBase, writeVerifyFacts, formatPlatformPathNote, backupVerifyResult, refreshProbeSections } = await import('./verify-probes.js');
1337
+ const { runVerifyProbes, renderVerifyProbesReport, generateVerifyResultSkeleton, resolveVerifyProbesSpecBase, writeVerifyFacts, formatPlatformPathNote, backupVerifyResult, refreshProbeSections, applySkeletonPreservingHuman } = await import('./verify-probes.js');
1338
1338
  // spec 根统一走漂移锚定(坑 worktree-spec-artifact-misplace):在 worktree 内跑时锚回主仓,
1339
1339
  // 探针读取与 --init 骨架都落主仓——与 plan/execute/verify/archive 的 command.js 守卫同口径。
1340
1340
  // resolvePlatformSpecDir 仍先调(保留平台接管 fail-closed 检查副作用),但仅 pointer 存在时
@@ -1429,21 +1429,29 @@ task done 四合一(r5l 方案1):review write(落 review.json+自动勾
1429
1429
  await mirrorInitArtifact(vpReportPath, 'verify-result.md', vpInjected);
1430
1430
  console.log(`\n📄 存量旧格式 verify-result.md 已补注入探针预填段: ${vpReportPath}(正文其余未动;补注后按新预填段如实核对结论)${vpPlatformNote}`);
1431
1431
  } else if (vpForce) {
1432
- // quick-B:--force 覆盖重生成全骨架(含接口矩阵等 CLI 预填段)——手填内容会重置。
1432
+ // quick-B:--force 覆盖重生成全骨架(含接口矩阵等 CLI 预填段)。
1433
1433
  // 2026-10-06-verify-friction-fix:覆盖前自动备份(postmortem 实证 38 格手填复核成果
1434
1434
  // 被清后只能靠对话记录重建——「从 git 或备份找回」的话术备份此前并不存在);备份
1435
1435
  // 失败时醒目提示手动复制再重跑(fail-soft 但不静默)。
1436
+ // 2026-10-09-verify-papercuts ②:语义升级——机器段全量重刷 + 人工面按段携载
1437
+ // (applySkeletonPreservingHuman:结论槽/移交项表/矩阵已填行不再被 wipe),备份
1438
+ // 保留为兜底通道。
1436
1439
  const { resolveRuntimeRoot: vpForceRTR } = await import('./run/shared.js');
1437
1440
  const vpBackupPath = backupVerifyResult({ mdPath: vpReportPath, runtimeRoot: vpForceRTR({}, vpSpecBase), changeName: vpChange });
1438
1441
  const vpSkeleton = generateVerifyResultSkeleton(vpResult);
1439
- writeFileSync(vpReportPath, vpSkeleton);
1440
- await mirrorInitArtifact(vpReportPath, 'verify-result.md', vpSkeleton);
1442
+ const vpOldText = readFileSync(vpReportPath, 'utf8');
1443
+ const vpApplied = applySkeletonPreservingHuman(vpOldText, vpSkeleton);
1444
+ writeFileSync(vpReportPath, vpApplied.text);
1445
+ await mirrorInitArtifact(vpReportPath, 'verify-result.md', vpApplied.text);
1441
1446
  if (vpBackupPath) {
1442
- console.log(`\n🗄️ --force 覆盖前已自动备份: ${vpBackupPath}(手填内容被重置时从这里找回回填)`);
1447
+ console.log(`\n🗄️ --force 覆盖前已自动备份: ${vpBackupPath}(携载语义之外的极端形态从这里找回)`);
1443
1448
  } else {
1444
1449
  console.error(`\n⚠️ 自动备份失败——若 verify-result.md 含手填内容,请立即手动复制保存后重跑本命令。`);
1445
1450
  }
1446
- console.log(`\n📄 --force 已重生成 verify-result.md 骨架: ${vpReportPath}(探针与预填段已刷新;⚠️ 手填的结论/移交项等已被重置——需保留的内容从上方备份找回后回填)${vpPlatformNote}`);
1451
+ const vpCarriedNote = vpApplied.carriedSections.length > 0
1452
+ ? `已填人工面按段携载保留:${vpApplied.carriedSections.join('、')}(携载表格行 ${vpApplied.carriedRows})——与机器段新渲染不符的携载内容由 gate 一致性抽查把关`
1453
+ : `未发现已填人工面(全文占位形态——行为与旧版全量重置一致)`;
1454
+ console.log(`\n📄 --force 已重生成 verify-result.md 骨架: ${vpReportPath}(探针与预填段已刷新;${vpCarriedNote})${vpPlatformNote}`);
1447
1455
  } else {
1448
1456
  console.log(`\nℹ️ verify-result.md 已存在,不覆盖: ${vpReportPath}${vpPlatformNote}(预填段过期需刷新时用 --init --force)`);
1449
1457
  }
@@ -2172,9 +2180,10 @@ task done 四合一(r5l 方案1):review write(落 review.json+自动勾
2172
2180
  const fpSpecBase = resolvePlatformSpecDir(dir, specDir) || join(dir, '.sillyspec');
2173
2181
  const fpDir = join(fpSpecBase, 'changes', fpChange);
2174
2182
  mkdirSync(fpDir, { recursive: true });
2175
- const fpNow = new Date();
2176
- const fpDate = fpNow.toISOString().slice(0, 10);
2177
- const fpStamp = fpNow.toISOString().slice(0, 19).replace('T', ' ');
2183
+ // 本地墙钟(datetime.js 坑 taskcard-created-at-utc 同族:frontmatter 人读时间禁
2184
+ // toISOString 写 UTC 裸形状——无时区标记被读取端按本地解析,2026-10-09 平台时间线 born 锚偏 8h 实证)
2185
+ const { nowWallClock } = await import('./datetime.js');
2186
+ const fpStamp = nowWallClock();
2178
2187
  const generated = [];
2179
2188
  const mk = (fname, body) => {
2180
2189
  const p = join(fpDir, fname);
@@ -3373,8 +3382,11 @@ ${generated.length} 个骨架已就绪——逐节把 <!--TODO--> 替换为语
3373
3382
  else if (wtCommitFlags[i] === '--pathspec-from-file' && wtCommitFlags[i + 1]) wtCommitPathspecFile = wtCommitFlags[++i];
3374
3383
  }
3375
3384
  if (!wtCommitChange) {
3376
- const inferred = String(dir).match(/[\\/]worktrees[\\/]([^\\/]+)/);
3377
- wtCommitChange = inferred ? inferred[1] : null;
3385
+ // FR-08(2026-10-09-verify-reuse-friction / D-006@v1):跨仓 <change>--<repoKey>
3386
+ // 注册表校验剥后缀——逻辑单点在 wt-commit.js(导出 inferChangeFromWorktreeCwd
3387
+ // 供三态回归直测,index.js dispatch 不留内联逻辑)
3388
+ const { inferChangeFromWorktreeCwd } = await import('./wt-commit.js')
3389
+ wtCommitChange = await inferChangeFromWorktreeCwd(dir)
3378
3390
  }
3379
3391
  try {
3380
3392
  const { runWtCommit } = await import('./wt-commit.js');
@@ -155,7 +155,9 @@ export function syncModuleDocSidecars({ cwd, changeName, note, specDir = null })
155
155
  const cardPath = join(modulesDir, `${mod}.md`)
156
156
  try {
157
157
  const card = readFileSync(cardPath, 'utf8')
158
- const stamped = card.replace(/^updated_at:.*$/m, `updated_at: ${new Date().toISOString().slice(0, 19) + '+08:00'}`)
158
+ // 全量 ISO(机器可比时间戳,scan-postcheck 同字段同口径)——旧式 UTC 数字拼硬编码
159
+ // '+08:00' 的瞬间恒早真实时刻 8h(数字取 UTC、偏移写死,机器无关地错)
160
+ const stamped = card.replace(/^updated_at:.*$/m, `updated_at: ${new Date().toISOString()}`)
159
161
  if (stamped !== card) writeFileSync(cardPath, stamped)
160
162
  } catch { /* 卡不存在——sidecar 照写,卡内容留给 agent */ }
161
163
  synced.push(proj ? `${proj}/${mod}` : mod)
@@ -369,12 +369,19 @@ export class StageMachine {
369
369
  if (!(isProgress || f.startsWith('spec-sync-conflict-')) || !f.endsWith('.json')) continue;
370
370
  let change = f.replace(/^spec-sync-conflict-|^sync-conflict-/, '').replace(/\.json$/, '');
371
371
  let createdAt = null;
372
+ let kind = null;
372
373
  try {
373
374
  const cf = JSON.parse(readFileSync(join(runtimeDir, f), 'utf8'));
374
375
  change = cf.change || change;
375
376
  createdAt = cf.created_at || null;
377
+ kind = typeof cf.kind === 'string' ? cf.kind : null;
376
378
  } catch { /* 损坏文件按文件名兜底,不崩 */ }
377
- conflicts.push({ change, created_at: createdAt, type: isProgress ? 'progress' : 'spec-tree' });
379
+ // 2026-10-09-tombstone-conflict-root-fix FR-01:kind='tombstone' 优先透传
380
+ // (归因记账新形态——daemon 摘要/前端据此区分墓碑冲突与版本冲突);文件名
381
+ // 前缀判定保留兜底(旧格式/损坏文件)。
382
+ const type = kind === 'tombstone' ? 'tombstone'
383
+ : isProgress ? 'progress' : 'spec-tree';
384
+ conflicts.push({ change, created_at: createdAt, type });
378
385
  }
379
386
  return conflicts;
380
387
  }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * code-face-key.js — 代码面口径单点(2026-10-09-verify-reuse-friction FR-03 / D-002@v1)。
3
+ *
4
+ * 问题:两类复用指纹(green-cache 的 computeGateFingerprint / 质量扫描的
5
+ * computeQualityScanFingerprint)原先以 git HEAD 为「代码态」分量——收口窗口内任何提交
6
+ * (哪怕纯文档的声明修正)都换 HEAD → 缓存必 miss(2026-10-09 multi-agent-platform
7
+ * tombstone 取证:13:35-14:10 五笔提交每笔击穿,2-3 轮 ×3.5min 重测纯浪费)。
8
+ *
9
+ * 修复:HEAD 分量替换为「代码树内容键」——`git ls-tree -r -z HEAD` 条目按既有非代码
10
+ * 路径口径(.sillyspec/、docs/、*.md——与 filterCodePorcelain 同源判据)过滤后哈希。
11
+ * ls-tree 条目自带 git 对象哈希(内容寻址):纯文档提交不进过滤集 → 键不变 → 缓存存活;
12
+ * 代码提交必进/必变 → 键变 → 必失配。整树 oid(HEAD^{tree})作快路径:oid 与上次相同
13
+ * → 直接沿用上次键(纯缓存优化,语义不变——oid 相同 ⇔ 整树逐字节相同)。
14
+ *
15
+ * 红线合规:非代码判据是**路径口径**(沿既有单点),不是语言/框架/扩展名枚举;
16
+ * ls-tree 是 git 对象枚举,与语言无关。-z(NUL 分隔)输出不加引号不转义——非 ASCII
17
+ * 路径的 endsWith 判据不被尾部引号击穿(独立审查指出的 quotepath 边界)。
18
+ *
19
+ * fail-open:git 不可用/任何异常 → null(调用方按 miss 处理,多跑一轮不误复用——
20
+ * 与两指纹既有的 git 不可用语义同向)。
21
+ */
22
+ import { execFileSync } from 'node:child_process';
23
+ import { createHash } from 'node:crypto';
24
+
25
+ function safeGit(cwd, args) {
26
+ try {
27
+ return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 60000, windowsHide: true });
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * 非代码路径判据(口径单点):.sillyspec/、docs/ 与 *.md——verify 窗口内的合法产出
35
+ * (规范文档/声明修正)不算代码面。green-cache.filterCodePorcelain 与质量扫描
36
+ * porcelainCodeLines 均消费本判据,不再各自实现。
37
+ */
38
+ export function isNonCodePath(p) {
39
+ return p.startsWith('.sillyspec/') || p.startsWith('docs/') || /\.md$/i.test(p);
40
+ }
41
+
42
+ /**
43
+ * porcelain 行过滤为「代码面」(原 green-cache.js 实现迁入,语义不变):
44
+ * rename 行「old -> new」两侧任一是文档面即整行剔除(防 rename 伪装穿透);排序保序。
45
+ */
46
+ export function filterCodePorcelain(porcelain) {
47
+ return String(porcelain || '')
48
+ .split(/\r?\n/)
49
+ .filter((line) => {
50
+ if (!line.trim()) return false;
51
+ const raw = line.slice(3).trim();
52
+ if (!raw) return false;
53
+ const parts = raw.split(' -> ').map((p) => p.replace(/^"|"$/g, ''));
54
+ return !parts.every((p) => isNonCodePath(p));
55
+ })
56
+ .sort();
57
+ }
58
+
59
+ /** 整树 oid 快路径缓存:cwd → { treeOid, key }(模块级;进程短命令生命周期内有效) */
60
+ const treeKeyMemo = new Map();
61
+
62
+ /**
63
+ * 代码树内容键:`git ls-tree -r -z HEAD` 条目(mode type oid\tpath)过滤非代码路径后
64
+ * 对逐条目原文(含 git 对象哈希——内容寻址)做 sha256。
65
+ * @returns {string|null} git 不可用/无 HEAD → null(调用方 fail-open miss)
66
+ */
67
+ export function computeCodeTreeKey({ cwd }) {
68
+ const treeOid = safeGit(cwd, ['rev-parse', 'HEAD^{tree}']);
69
+ if (treeOid === null) return null;
70
+ const oid = treeOid.trim();
71
+ const memo = treeKeyMemo.get(cwd);
72
+ if (memo && memo.treeOid === oid) return memo.key;
73
+ const out = safeGit(cwd, ['ls-tree', '-r', '-z', 'HEAD']);
74
+ if (out === null) return null;
75
+ const entries = String(out).split('\0').filter((e) => {
76
+ if (!e) return false;
77
+ const tab = e.indexOf('\t');
78
+ if (tab === -1) return false;
79
+ return !isNonCodePath(e.slice(tab + 1));
80
+ });
81
+ const key = createHash('sha256').update(entries.join('\n')).digest('hex');
82
+ treeKeyMemo.set(cwd, { treeOid: oid, key });
83
+ return key;
84
+ }
@@ -1126,7 +1126,8 @@ async function printExecuteScopeAudit({ cwd, changeName, specBase, platformOpts,
1126
1126
  snap.closedBy = 'execute --done'
1127
1127
  // 沉淀资产面投影(2026-10-07-unify-close-trace):files/totals = 主仓实改行(planned+
1128
1128
  // unplanned;untouched 0/0 是声明不是改动、crossRepo 行 patch 不在主仓——均不入)。
1129
- // 经共用 writeCloseTraceArtifacts 与 thin flow done 同一处写四件(sha256 双套同锚);
1129
+ // 经共用 writeCloseTraceArtifacts 与 thin flow done 同一处单套写两件(2026-10-09-
1130
+ // close-trace-single-set:对账面并入 change-patch.json 的 scopeAudit 子对象);
1130
1131
  // 投影口径抽 flow-parity.projectTraceFaceRows 纯函数(排除路径有单测钉住)。
1131
1132
  const face = projectTraceFaceRows(snap.rows)
1132
1133
  const closeTrace = writeCloseTraceArtifacts({
@@ -1143,7 +1144,7 @@ async function printExecuteScopeAudit({ cwd, changeName, specBase, platformOpts,
1143
1144
  note: 'execute --done 时点冻结(沉淀资产面:主仓实改行投影——thin flow done 对等;治理工件目录与其后 verify/archive 演进不在内)',
1144
1145
  },
1145
1146
  })
1146
- console.log(` 📦 范围快照 + 沉淀资产双轨落盘(scope-audit.json${closeTrace.patchStatus === 'ok' ? ' + scope-audit.patch' : ''} + change-patch.json${closeTrace.patchStatus === 'ok' ? ' + change.patch' : ''}${closeTrace.patchStatus === 'ok' ? '——收尾时点冻结,sha256 同锚' : '——patch 采集失败已留痕'})`)
1147
+ console.log(` 📦 范围快照落盘(单套):change-patch.json${closeTrace.patchStatus === 'ok' ? ' + change.patch' : ''}(对账面并入 scopeAudit 子对象${closeTrace.patchStatus === 'ok' ? ',收尾时点冻结 sha256 已锚' : '——patch 采集失败已留痕'})`)
1147
1148
  } catch (e) {
1148
1149
  console.warn(` ⚠️ 范围对账快照写入失败(不阻断,verify 漂移对比将按无快照降级):${e && e.message ? String(e.message).split('\n')[0] : e}`)
1149
1150
  }
@@ -1161,13 +1162,21 @@ async function printVerifyScopeDrift({ cwd, changeName, specBase, platformOpts,
1161
1162
  return
1162
1163
  }
1163
1164
  let snapshot = null
1164
- // 快照读取链(quick-359a48f1):变更目录(审计级,execute --done 落)→ .runtime(存量兼容)
1165
+ // 快照读取链(2026-10-09-close-trace-single-set):change-patch.json.scopeAudit(单套
1166
+ // 新形态)→ 旧 scope-audit.json(存量归档)→ .runtime/scope-audit-<change>.json(更早
1167
+ // 存量兼容)——scopeAuditPathSet 只吃 rows,子对象与旧顶级形态同构直用
1165
1168
  try {
1166
- snapshot = JSON.parse(readFileSync(join(specBase, 'changes', changeName, 'scope-audit.json'), 'utf8'))
1167
- } catch {
1169
+ const merged = JSON.parse(readFileSync(join(specBase, 'changes', changeName, 'change-patch.json'), 'utf8'))
1170
+ if (merged && typeof merged.scopeAudit === 'object' && merged.scopeAudit) snapshot = merged.scopeAudit
1171
+ } catch { /* 新形态缺失 → 旧名兜底 */ }
1172
+ if (!snapshot) {
1168
1173
  try {
1169
- snapshot = JSON.parse(readFileSync(join(resolveRuntimeRoot(platformOpts, specBase), `scope-audit-${changeName}.json`), 'utf8'))
1170
- } catch { /* 快照缺失/损坏 → 跳过对比只打当前 totals */ }
1174
+ snapshot = JSON.parse(readFileSync(join(specBase, 'changes', changeName, 'scope-audit.json'), 'utf8'))
1175
+ } catch {
1176
+ try {
1177
+ snapshot = JSON.parse(readFileSync(join(resolveRuntimeRoot(platformOpts, specBase), `scope-audit-${changeName}.json`), 'utf8'))
1178
+ } catch { /* 快照缺失/损坏 → 跳过对比只打当前 totals */ }
1179
+ }
1171
1180
  }
1172
1181
  if (!snapshot || snapshot.ok !== true) {
1173
1182
  console.log(` ${scopeLine}(无 execute 时点快照可对比——完整表跑 sillyspec scope-audit --change ${changeName})`)
@@ -41,6 +41,25 @@ function git(cwd, args) {
41
41
  return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 60000, windowsHide: true }).trim()
42
42
  }
43
43
 
44
+ /**
45
+ * 快照回退主仓的可见性 + 摩擦记账(2026-10-09-verify-reuse-friction FR-02)。
46
+ * 2026-10-09 取证:快照创建慢性失败静默回退主仓口径——apply 前实测的是「无变更代码」的
47
+ * HEAD 且绿结论可被 --done 复用消费(正确性风险),全程零可见零记账,只能靠 .runtime 考古
48
+ * 发现。本 helper 统一两处调用点(gates.js verify 实测门 / verify-quality-scan.js noAI 动作)
49
+ * 的告警块与 gate_snapshot_fallback 摩擦事件;记账 best-effort 不影响门禁。
50
+ */
51
+ export async function reportGateSnapshotFallback({ cwd, changeName, platformOpts = null, where, reason }) {
52
+ console.warn(`⚠️ [gate-snapshot-fallback] ${where}:隔离快照创建失败——实测回退主仓口径。`)
53
+ console.warn(` 影响并行 WIP 可能混入判定;apply 前主仓 HEAD 可能不含本变更代码——实测结论须按主仓口径解读,收口前确认变更代码已落主仓。`)
54
+ console.warn(` 原因:${reason}`)
55
+ console.warn(` 排查:git worktree 可用性(.git/worktrees 可写)与变更文件集非空;确要主仓口径显式设 SILLYSPEC_VERIFY_GATE_SNAPSHOT_OFF=1(口径切换会使复用指纹失配一次后收敛)。`)
56
+ try {
57
+ const { recordFrictionEvent } = await import('../friction-tally.js')
58
+ await recordFrictionEvent({ cwd, platformOpts, changeName, type: 'gate_snapshot_fallback', detail: `${where}: ${String(reason).slice(0, 160)}` })
59
+ } catch { /* 记账失败不影响门禁 */ }
60
+ }
61
+
62
+
44
63
  /** worktree 注册是否仍登记该 root(跨平台路径归一 + win32 大小写不敏感) */
45
64
  function worktreeListContains(cwd, snapshotRoot) {
46
65
  const out = git(cwd, ['worktree', 'list', '--porcelain'])