sillyspec 3.32.2 → 3.32.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.
@@ -51,4 +51,4 @@ sillyspec run auto --input "<用户需求>" [--mode <模式>]
51
51
  ### 边界
52
52
 
53
53
  - 中断恢复:直接重跑 `sillyspec run auto`(进度已落盘,从断点续)。
54
- - 分类为 quick 的小变更:CLI 会提示改跑 `sillyspec run quick`,照提示执行。
54
+ - 分类为小变更时 CLI 会提示改走轻量变更 `sillyspec flow start`,照提示执行。
@@ -67,12 +67,12 @@ sillyspec run brainstorm --done --answer "..." --output "..." # 一步完成
67
67
  ```
68
68
  ┌─ scale=large → plan(四件套齐)
69
69
  scan → brainstorm ┤
70
- └─ scale=small → quick --linked-changes(仅 design.md)
70
+ └─ scale=small → flow start 收编(仅 design.md)
71
71
  ```
72
72
 
73
73
  brainstorm 完成时按 design.md frontmatter 的 `scale` 分叉:
74
74
  - **large**(多文件/跨模块/有状态机或 schema 变更):四件套齐 + Design Grill 审查通过(tier=independent 时由独立审查子代理产出 stage review.json)→ `sillyspec run plan --change <变更名>`
75
- - **small**(≤2 文件、单模块、无跨模块依赖):仅生成 design.md → `sillyspec run quick --linked-changes <变更名>`
75
+ - **small**(≤2 文件、单模块、无跨模块依赖):仅生成 design.md → `sillyspec flow start --change <变更名>` 收编续跑(design 以头脑风暴版为准)
76
76
 
77
77
  > 规模由 AI 在 brainstorm 最后一步评估并写入 design.md frontmatter。判错可手动改 `scale` 后再跑相应阶段。
78
78
 
@@ -108,6 +108,7 @@ brainstorm 在 `tier=independent` 规模下除 design.md 等四件套外,还
108
108
  - **必须用 exec 工具(shell)执行 CLI,不要自己编造流程**
109
109
  - 只做当前步骤 prompt 描述的操作,不跳过、不自行扩展
110
110
  - 产物写入 CLI 输出的 `changeDir` 目录(如 `<changeDir>/design.md`),不要自己拼路径
111
+ - **变更标题(面板显示用):总结一句中文概括,≤50 字、建议 ~20 字**——CLI 从 proposal.md / design.md 首行 H1 提取简述写入 changes.title(剥「提案书(Proposal)— / 设计文档(Design)—」固定前缀后取段),design.md 固定格式 `# 设计文档(Design)— <简述>` 中的简述即面板标题,按此口径写
111
112
  - 完成后立即 `--done`,不跳过
112
113
 
113
114
  ## 用户指令
@@ -79,8 +79,6 @@ sillyspec run explore --status # 查看阶段状态
79
79
 
80
80
  **暴露风险**:识别可能出错的地方、发现理解空白
81
81
 
82
- ## OpenSpec 上下文感知
83
-
84
82
  ### 检查已有上下文
85
83
 
86
84
  ```bash
@@ -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/README.md CHANGED
@@ -85,7 +85,7 @@ SillySpec 进度库使用 SQLite 持久化(基于 Node.js 内置的 `node:sqli
85
85
 
86
86
  **也可以跳过完整流程:**
87
87
 
88
- - 小改动:`/sillyspec:quick "修复 xxx"`
88
+ - 小改动:`/sillyspec:flow "修复 xxx"`
89
89
  - 不确定做什么:`/sillyspec:explore`
90
90
  - 中断恢复:`/sillyspec:resume`
91
91
  - 不知道下一步:`/sillyspec:continue`
@@ -112,7 +112,7 @@ SillySpec 进度库使用 SQLite 持久化(基于 Node.js 内置的 `node:sqli
112
112
  | `/sillyspec:status` · `/sillyspec:state` | 查看进度 / 当前工作状态 |
113
113
  | `/sillyspec:continue` | 自动判断并执行下一步 |
114
114
  | `/sillyspec:explore` | 自由思考:画图、讨论、调研、HTML 原型 |
115
- | `/sillyspec:quick` | 快速模式:跳过完整流程 |
115
+ | `/sillyspec:flow` | 轻量变更(默认快道):需求明确的小改动两步收口 |
116
116
  | `/sillyspec:resume` | 恢复工作(支持大模块阶段进度) |
117
117
  | `/sillyspec:doctor` | 项目自检与状态修复 |
118
118
  | `/sillyspec:commit` | 智能提交 |
@@ -122,7 +122,7 @@ SillySpec 进度库使用 SQLite 持久化(基于 Node.js 内置的 `node:sqli
122
122
  ## CLI 命令
123
123
 
124
124
  ```bash
125
- sillyspec run <stage> 执行阶段(auto/brainstorm/plan/execute/verify/archive/scan/quick/explore)
125
+ sillyspec run <stage> 执行阶段(auto/brainstorm/plan/execute/verify/archive/scan/explore)
126
126
  sillyspec run <stage> --done 完成当前步骤并推进到下一步
127
127
  sillyspec run <stage> --status 查看阶段进度
128
128
  sillyspec progress show 显示当前项目状态
@@ -140,7 +140,7 @@ sillyspec change-delete <变更名> 删除变更(DB status=deleted 与归
140
140
  **第一层:护城河(外部状态,模型能力不可替代)**
141
141
 
142
142
  - **多 agent 并发控制** — 会话/变更/worktree 三级隔离;文件锁 ID 分配 + 双占用 fail-closed 硬拦;`--files` 边界声明切分"这是谁改的";`--done` 前并发写预检告警他者未提交改动;平台同步乐观锁(base_ts + 身份 + 服务器权威钟)+ 冲突血统归属(自回声不误报)
143
- - **实测验收门禁** — verify / quick 收尾时 CLI **亲自执行** test / lint 与 agent 自报对账,不符即阻断回滚;docHash 重算比对 + git 提交证据交叉对账防伪造;门禁跑在隔离快照,不受并行会话脏文件污染
143
+ - **实测验收门禁** — verify 收尾时 CLI **亲自执行** test / lint 与 agent 自报对账,不符即阻断回滚;docHash 重算比对 + git 提交证据交叉对账防伪造;门禁跑在隔离快照,不受并行会话脏文件污染
144
144
  - **持久账本 + 断点恢复** — SQLite(node:sqlite)单一进度源,进度/决策/审批答复全落库,跨会话跨天续跑;quicklog 四字段结构化台账可检索
145
145
  - **跨仓变更管理** — task 卡声明 `repo:` 字段即可管理跨仓任务:同构 worktree 隔离、base 锚快照、apply 永不覆盖用户在途改动
146
146
 
package/SKILL.md CHANGED
@@ -51,7 +51,6 @@ Claude Code / Cursor / Codex / OpenCode / OpenClaw / Gemini 通用。
51
51
  | `/sillyspec:continue` | 自动判断并执行下一步 |
52
52
  | `/sillyspec:explore` | 自由思考模式 |
53
53
  | `/sillyspec:flow` | 轻量变更(默认快道):flow start → 干活 → flow done |
54
- | `/sillyspec:quick` | (已退役,重定向到 flow)仅存量在途会话收尾 |
55
54
  | `/sillyspec:resume` | 恢复工作 |
56
55
  | `/sillyspec:doctor` | 项目自检 |
57
56
  | `/sillyspec:commit` | 智能提交 |
@@ -70,7 +69,6 @@ sillyspec run execute 执行开发阶段(子代理并行 + worktree
70
69
  sillyspec run verify 执行验证阶段
71
70
  sillyspec run archive 执行归档阶段
72
71
  sillyspec flow start/done/status 轻量变更(默认快道,2 次调用收口)
73
- sillyspec run quick (已退役,仅存量在途会话 --done/--cancel 收尾)
74
72
  sillyspec run explore 自由探索
75
73
  sillyspec progress show 显示当前项目状态
76
74
  sillyspec setup 安装推荐 MCP 工具
@@ -81,12 +79,12 @@ sillyspec init 初始化(零交互,自动检测工具)
81
79
 
82
80
  **护城河层(外部状态,模型能力不可替代)**
83
81
  - **多 agent 并发控制** — 会话/变更/worktree 三级隔离;文件锁 ql-ID 分配 + 双占用 fail-closed 硬拦;边界声明归属切分(窗口∩声明);并发写预检告警;平台乐观锁(base_ts + 身份头 + 服务器权威钟)
84
- - **实测验收门禁** — verify/quick 收尾时 CLI 亲自执行 test/lint 与自报对账,不符即阻断回滚;docHash 重算 + git 证据交叉对账防伪造;门禁跑在隔离快照里不受并行会话脏文件污染
82
+ - **实测验收门禁** — verify 收尾时 CLI 亲自执行 test/lint 与自报对账,不符即阻断回滚;docHash 重算 + git 证据交叉对账防伪造;门禁跑在隔离快照里不受并行会话脏文件污染
85
83
  - **持久账本 + 断点恢复** — SQLite 单一进度源,进度/决策/审批答复全落库,跨会话跨天续跑;quicklog 四字段结构化台账
86
84
  - **跨仓变更管理** — task 卡声明 `repo:` 字段,跨仓同构 worktree 隔离,base 锚快照,apply 不覆盖用户在途改动
87
85
 
88
86
  **确定性对账层(机械核验,零模型轮次)**
89
- - **范围对账** — 计划改动 × 实际改动三态对账(planned/unplanned/untouched),execute/verify/archive/quick 四处收尾注入
87
+ - **范围对账** — 计划改动 × 实际改动三态对账(planned/unplanned/untouched),execute/verify/archive 三处收尾注入
90
88
  - **API 契约矩阵** — provider/consumer 识别、endpoint 工件提取、前后端 API parity 对账
91
89
  - **文档一致性棘轮** — 源码引用真实性校验(HEAD 模式),失败数只降不升
92
90
  - **noAI 下沉 / IR 事实层** — 机械轮次下沉 CLI(骨架预生成、注入、代算对账),判断层留给 agent;同一契约清单事前渲染预览、事后引擎核验
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sillyspec",
3
- "version": "3.32.2",
3
+ "version": "3.32.4",
4
4
  "description": "SillySpec CLI — 流程状态机,让 AI 严格按步骤来",
5
5
  "icon": "logo.jpg",
6
6
  "homepage": "https://sillyspec.ppdmq.top/",
@@ -10,7 +10,7 @@ import { writeAtomicSync } from './fs-atomic.js'
10
10
 
11
11
  export const COMMAND_CARD_TARGETS = { zcode: '.zcode/commands/sillyspec', claude: '.claude/commands/sillyspec' }
12
12
  // flow=轻量变更默认道入口卡(2026-09-25-quick-channel-retire 配套:quick 退役后快道首次有卡)
13
- export const COMMAND_CARD_NAMES = ['flow', 'run-brainstorm', 'run-plan', 'run-execute', 'run-verify', 'run-archive', 'run-quick', 'status']
13
+ export const COMMAND_CARD_NAMES = ['flow', 'run-brainstorm', 'run-plan', 'run-execute', 'run-verify', 'run-archive', 'status']
14
14
 
15
15
  const ANCHOR_TAIL_RE = /\n?<!-- sillyspec-card: v(\S+) sha256=([0-9a-f]{64}) -->\s*$/
16
16
  const sha256 = (s) => createHash('sha256').update(s, 'utf8').digest('hex')
@@ -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'
@@ -220,7 +221,7 @@ export function filterCommittedFace(committedRaw, ownPrefix, opts = {}) {
220
221
 
221
222
  /**
222
223
  * patch 冻结面漂移检测(2026-10-05-disposition-refreeze-drift,主清单 P1):处置重入审计
223
- * 错位——评审发现处置涉及代码修改后重跑 flow done,patch 子步幂等跳过让 change.patch/
224
+ * 时点错位——评审发现处置涉及代码修改后重跑 flow done,patch 子步幂等跳过让 change.patch/
224
225
  * review.json 停在处置前时点(2026-10-05-review-promise-negation 先例 905397ef;
225
226
  * 2026-10-05-flowdone-lintfail-output 收编活体复现:评审 P1 处置提交后不带 --refreeze 重跑,
226
227
  * 归档件缺处置面)。
@@ -229,23 +230,41 @@ export function filterCommittedFace(committedRaw, ownPrefix, opts = {}) {
229
230
  * 漂移——按提交 message 变更名归属(parseChangeNamesFromSubject,与 filterCommittedFace
230
231
  * 同口径的既成事实切分,非声明抢文件)。仅他侧后缀/裸提交不触发(他侧面归属切分另有防线,
231
232
  * 裸提交保守不动作)。检测自身失败按无漂移(退现状幂等跳过行为,fail-safe 不新造阻断面)。
233
+ *
234
+ * 文件面三字段(2026-10-10-drift-review-governance-keep,坑 drift-review-governance-loop):
235
+ * ownCommits 只答「有没有漂移」,不答「评审结论该不该作废」——调用方(flow.js 漂移分支)
236
+ * 据文件面区分治理面等价(评审保留)与交付/承诺面(隔离重评):
237
+ * ownFiles 窗口内本变更提交触及的文件集(--name-only 采集,正斜杠归一;merge 提交
238
+ * 无 -m 展开不计文件——漏采只可能让 governanceOnly 偏 false,回落隔离
239
+ * 重评,fail-safe 宁可多评)
240
+ * governanceOnly own 提交全在 .sillyspec/** 下且文件集非空(治理文件等价漂移)
241
+ * touchedPromiseFace 含本变更 requirements.md / design.md(承诺面也是评审对象,破豁免)
232
242
  * @param {{ cwd: string, change: string, freezeHead: string|null }} opts
233
- * @returns {{ drifted: boolean, ownCommits: string[], head: string|null }} ownCommits=本变更后缀提交的 subject 列表(截 80 字)
243
+ * @returns {{ drifted: boolean, ownCommits: string[], head: string|null, ownFiles: string[], governanceOnly: boolean, touchedPromiseFace: boolean }} ownCommits=本变更后缀提交的 subject 列表(截 80 字)
234
244
  */
235
245
  export function detectPatchDrift({ cwd, change, freezeHead }) {
236
246
  try {
237
- if (!freezeHead) return { drifted: false, ownCommits: [], head: null }
247
+ if (!freezeHead) return { drifted: false, ownCommits: [], head: null, ownFiles: [], governanceOnly: false, touchedPromiseFace: false }
238
248
  const head = execFileSync('git', ['rev-parse', 'HEAD'], { cwd, encoding: 'utf8', timeout: 30000, windowsHide: true }).trim()
239
- if (!head || head === freezeHead) return { drifted: false, ownCommits: [], head }
240
- const log = execFileSync('git', ['log', '--format=%x00%s', `${freezeHead}..HEAD`], { cwd, encoding: 'utf8', timeout: 30000, windowsHide: true })
249
+ if (!head || head === freezeHead) return { drifted: false, ownCommits: [], head, ownFiles: [], governanceOnly: false, touchedPromiseFace: false }
250
+ const log = execFileSync('git', ['log', '--name-only', '--format=%x00%s', `${freezeHead}..HEAD`], { cwd, encoding: 'utf8', timeout: 30000, windowsHide: true })
241
251
  const own = []
252
+ const ownFileSet = new Set()
242
253
  for (const block of String(log).split('\x00')) {
243
- const subject = block.split('\n').map((l) => l.trim()).filter(Boolean)[0]
254
+ const lines = block.split('\n').map((l) => l.trim()).filter(Boolean)
255
+ const subject = lines[0]
244
256
  if (!subject) continue
245
- if (parseChangeNamesFromSubject(subject).includes(change)) own.push(subject.slice(0, 80))
257
+ if (parseChangeNamesFromSubject(subject).includes(change)) {
258
+ own.push(subject.slice(0, 80))
259
+ for (const f of lines.slice(1)) ownFileSet.add(f.replace(/\\/g, '/'))
260
+ }
246
261
  }
247
- return { drifted: own.length > 0, ownCommits: own, head }
248
- } catch { return { drifted: false, ownCommits: [], head: null } }
262
+ const ownFiles = [...ownFileSet]
263
+ const promiseFace = [`.sillyspec/changes/${change}/requirements.md`, `.sillyspec/changes/${change}/design.md`]
264
+ const governanceOnly = own.length > 0 && ownFiles.length > 0 && ownFiles.every((f) => f.startsWith('.sillyspec/'))
265
+ const touchedPromiseFace = ownFiles.some((f) => promiseFace.includes(f))
266
+ return { drifted: own.length > 0, ownCommits: own, head, ownFiles, governanceOnly, touchedPromiseFace }
267
+ } catch { return { drifted: false, ownCommits: [], head: null, ownFiles: [], governanceOnly: false, touchedPromiseFace: false } }
249
268
  }
250
269
 
251
270
  /**
@@ -358,21 +377,21 @@ export function projectTraceFaceRows(rows) {
358
377
  }
359
378
 
360
379
  /**
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
- * 齐备,回退退化为存量归档兜底。
380
+ * 收口留痕单套写(2026-10-09-close-trace-single-set):thin(flow done)与 heavy
381
+ * (execute --done)统一只落 change.patch + change-patch.json 两件。原对账快照面
382
+ * (scope-audit.json + scope-audit.patch,2026-10-07-unify-close-trace 起与沉淀资产面
383
+ * 双轨并写)停写:对账数据并入 change-patch.json 的 scopeAudit 子对象,顶级键
384
+ * (change/baseline/head/files/totals/savedAt/note/moduleScope 等)原位不动——
385
+ * fr-index/knowledge-graph/flow 漂移检测与平台 assets.py/parser.py 等顶级字段读方零改动。
386
+ * patchText 一次 sha256 锚定 patch 与 patchStatus;patchText 空/null → failed、不落
387
+ * patch 文件。读侧兼容链(scopeAudit 子对象 > 旧 scope-audit.json > .runtime 快照 >
388
+ * thin 回放 > 实时区间)兜存量归档,旧名只读不写。
370
389
  *
371
390
  * @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 后)
391
+ * @param {object} snapObj scopeAudit 子对象主体(mode/ok/degradedReason/baseAnchor/
392
+ * totals/rows/excluded/note[/repos];closedBy 由调用方携带标收尾通道——读侧回放按它
393
+ * 标注,缺省 'execute --done' 兼容旧快照)
394
+ * @param {object} meta change-patch.json 顶级增量键(note/moduleScope 等,展开在 totals 后)
376
395
  * @returns {{ patchStatus: 'ok'|'failed', patchSha256: string|null }}
377
396
  */
378
397
  export function writeCloseTraceArtifacts({ changeDir, change, baseline, head, files, metaTotals, patchText, savedAt, snapObj, meta = {} }) {
@@ -382,10 +401,7 @@ export function writeCloseTraceArtifacts({ changeDir, change, baseline, head, fi
382
401
  const patchSha256 = patchBody
383
402
  ? createHash('sha256').update(patchBody.replace(/\r\n/g, '\n'), 'utf8').digest('hex')
384
403
  : null
385
- if (patchOk) {
386
- writeFileSync(join(changeDir, 'change.patch'), patchBody)
387
- writeFileSync(join(changeDir, 'scope-audit.patch'), patchBody)
388
- }
404
+ if (patchOk) writeFileSync(join(changeDir, 'change.patch'), patchBody)
389
405
  writeFileSync(join(changeDir, 'change-patch.json'), JSON.stringify({
390
406
  change,
391
407
  baseline: baseline ?? null,
@@ -396,15 +412,10 @@ export function writeCloseTraceArtifacts({ changeDir, change, baseline, head, fi
396
412
  : { files: fileList.length, additions: 0, deletions: 0 },
397
413
  savedAt,
398
414
  ...meta,
415
+ scopeAudit: { ...snapObj },
399
416
  ...(patchSha256 ? { patchSha256 } : {}),
400
417
  patchStatus: patchOk ? 'ok' : 'failed',
401
418
  }, 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
419
  return { patchStatus: patchOk ? 'ok' : 'failed', patchSha256 }
409
420
  }
410
421
 
@@ -16,7 +16,7 @@
16
16
  * 豁免 ≠ 免责:全信号不命中而豁免的变更按 1/4 定额抽查采样(确定性哈希分桶),采样命中照评
17
17
  * 并进遥测——误豁免率可测量可校准(先例:review-dispatch shadow 影子期 N=10 转正判据)。
18
18
  */
19
- import { existsSync, readFileSync } from 'node:fs'
19
+ import { existsSync, readFileSync, readdirSync } from 'node:fs'
20
20
  import { join } from 'node:path'
21
21
  import { DESIGN_QUESTIONS as DESIGN_QUESTIONS_FALLBACK } from './flow-draft.js'
22
22
 
@@ -203,7 +203,17 @@ export function renderReviewerTaskbook({ change, changeDir, head = null }) {
203
203
  // (首评任务书与现状逐字一致)。
204
204
  let priorFindingsMd = ''
205
205
  try {
206
- const prev = JSON.parse(readFileSync(join(changeDir, 'review.json'), 'utf8'))
206
+ // 前轮产物读取(2026-10-10-drift-review-governance-keep,坑 drift-review-governance-loop ②):
207
+ // review.json 在场优先;漂移隔离后已改名 review.json.superseded-<ts>——缺席时回退字典序最新
208
+ // 隔离件(时间戳后缀同格式可排序),否则处置重评读不到前轮 findings,「已报项只验修复+
209
+ // 回归」复审语义丢失,退化为全量首评整轮重报(2026-10-10 用户实证)。changeDir 不存在/
210
+ // 无隔离件 → readFileSync/readdirSync 抛错走外层 catch,首评形态零注入不变。
211
+ let prevPath = join(changeDir, 'review.json')
212
+ if (!existsSync(prevPath)) {
213
+ const supers = readdirSync(changeDir).filter((f) => /^review\.json\.superseded-/.test(f)).sort()
214
+ if (supers.length > 0) prevPath = join(changeDir, supers[supers.length - 1])
215
+ }
216
+ const prev = JSON.parse(readFileSync(prevPath, 'utf8'))
207
217
  if (Array.isArray(prev.findings) && prev.findings.length > 0) {
208
218
  priorFindingsMd = [
209
219
  ``,
package/src/flow.js CHANGED
@@ -1272,6 +1272,14 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1272
1272
  } catch { /* 锚定判定失败按未锚定处理 */ }
1273
1273
  if (anchoredToHead) {
1274
1274
  console.log(` review.json 已锚定当前 HEAD(reviewedAgainst 命中 ${String(drift.head || '').slice(0, 8)})——评审对象即最新交付面,结论保留不隔离`)
1275
+ } else if (drift.governanceOnly && !drift.touchedPromiseFace) {
1276
+ // 治理面等价保留(2026-10-10-drift-review-governance-keep,坑 drift-review-governance-loop
1277
+ // 用户实证):评审结论的对象是交付面——窗口内本变更提交只碰 .sillyspec/** 治理文件且
1278
+ // 未触本变更 requirements/design(承诺面也是评审对象)时,结论对最新交付面逐字节等价,
1279
+ // 保留不隔离不重置标记;change.patch 仍走下方 driftRefreeze 自动重冻结吸收治理面增量
1280
+ // (审计件覆盖最新提交面不变量不破)。此前任何后缀提交一律隔离重评——评审后修一个
1281
+ // P2 文档措辞也付整轮子代理评审费(循环,被迫手工重排执行顺序自赎)。
1282
+ console.log(` review.json 保留不隔离——窗口内本变更提交仅触及治理文件(${drift.ownFiles.length} 个,未触 requirements/design),评审对象(交付面)未变,结论对最新面等价;change.patch 自动重冻结吸收治理面增量`)
1275
1283
  } else {
1276
1284
  const ts = new Date().toISOString().replace(/[-:]/g, '').replace(/\..+$/, '')
1277
1285
  try {
@@ -1421,6 +1429,10 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1421
1429
  console.warn(`⚠️ --accept-dirty-gap:显式接受 ${freeze.dirtyWarned.length} 个未提交交付文件不进冻结面(缺口数随 change-patch.json acceptedDirtyGap 留痕)`)
1422
1430
  }
1423
1431
  }
1432
+ // 收口留痕四名全排除(2026-10-09-close-trace-single-set 后只写 change.patch/
1433
+ // change-patch.json 两件,但旧轮冻结件里可能仍有 scope-audit.json/.patch——
1434
+ // legacy 防护保留):上一轮留痕件作为 untracked 新文件被全文自嵌入即自引用循环
1435
+ // (旧 patch 内容混进新 patch,三评 P1 根因)。
1424
1436
  const ownFiles = [...new Set([...freeze.files, ...changeDirFiles])]
1425
1437
  .filter((f) => f && !f.endsWith('change.patch') && !f.endsWith('change-patch.json')
1426
1438
  && !f.endsWith('scope-audit.json') && !f.endsWith('scope-audit.patch'))
@@ -1437,8 +1449,7 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1437
1449
  const toPosix = (p) => String(p).replace(/\\/g, '/')
1438
1450
  // 复审 P2/P3a 清偿:工作树口径集 = 治理工件目录(含已提交后又改的槽位/勾选——工作树
1439
1451
  // diff 能捕捉提交后编辑)∪ 独占树未提交交付(dirtyAdded);其余交付面走提交区间。
1440
- // 排除项与 ownFiles 同款(change.patch/change-patch.json)——否则上一轮冻结件作为
1441
- // untracked 新文件被全文自嵌入(自引用循环:旧 patch 内容混进新 patch,三评 P1 根因)。
1452
+ // 排除项与 ownFiles 同款(四名全排,含 scope-audit.* 旧轮冻结件 legacy 防护)。
1442
1453
  const dirFilesForPatch = changeDirFiles.filter((f) => !f.endsWith('change.patch') && !f.endsWith('change-patch.json')
1443
1454
  && !f.endsWith('scope-audit.json') && !f.endsWith('scope-audit.patch'))
1444
1455
  const worktreeSet = new Set([...dirFilesForPatch, ...((freeze.dirtyAdded) || [])].map(toPosix))
@@ -1469,10 +1480,9 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1469
1480
  if (rec.lines.length > 0) for (const l of rec.lines) console.log(l)
1470
1481
  moduleScope = { modules: rec.modules, uncoveredDirs: rec.uncoveredDirs, moduleMaps: rec.moduleMaps }
1471
1482
  } 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),新变更两条通道留痕对称,读侧回退退化为存量兜底。
1483
+ // 收口留痕单套写(2026-10-09-close-trace-single-set):共用 writeCloseTraceArtifacts
1484
+ // 只写 change.patch + change-patch.json 两件——对账面数据并入 JSON 的 scopeAudit
1485
+ // 子对象(thin 通道同样有对账面),旧名 scope-audit.* 停写、读侧兼容兜存量归档。
1476
1486
  let planEntries = []
1477
1487
  try {
1478
1488
  const { parseFileChangeListDetailed } = await import('./change-list.js')
@@ -1508,7 +1518,7 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1508
1518
  ...(acceptDirtyGap && freeze.dirtyWarned.length > 0 ? { acceptedDirtyGap: freeze.dirtyWarned.length } : {}),
1509
1519
  },
1510
1520
  })
1511
- console.log(`📦 变更 patch 留档(双轨统一):change.patch + change-patch.json(${ownFiles.length} 文件,+${additions}/-${deletions})+ scope-audit.json + scope-audit.patch(对账快照同点冻结)${closeTrace.patchStatus === 'ok' ? ',sha256 已锚' : '——patch 采集失败已留痕'}`)
1521
+ console.log(`📦 变更 patch 留档(单套):change.patch + change-patch.json(${ownFiles.length} 文件,+${additions}/-${deletions},对账面并入 scopeAudit 子对象)${closeTrace.patchStatus === 'ok' ? ',sha256 已锚' : '——patch 采集失败已留痕'}`)
1512
1522
  // design 声明面自证(2026-09-25-platform-feedback-batch2 E):design.md 文件变更清单
1513
1523
  // 声明的交付文件是否都在冻结面——不在=承诺改了但没交付(承诺未兑现面,advisory 不
1514
1524
  // 阻断——可能是范围裁剪了但 design 没同步更新)
@@ -1596,6 +1606,11 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1596
1606
  if (others.length > 0) {
1597
1607
  console.warn(`⚠️ 评审非阻断发现 ${others.length} 项(P2/P3,随归档留档):`)
1598
1608
  for (const f of others) console.warn(` [${f.severity}] ${f.title} — ${f.evidence || ''}`)
1609
+ // 处置提示(2026-10-10-drift-review-governance-keep,坑 drift-review-governance-loop):
1610
+ // P2/P3 非阻断可留债归档;要修则修复面决定是否重评——只碰治理文件不动本变更
1611
+ // requirements/design 的后缀提交触发治理面等价保留(评审保留+自动重冻结,零重评);
1612
+ // 触及交付面或承诺面才隔离重评(任务书带前轮 findings,复审只验修复+新增)。
1613
+ console.warn(` 💡 处置参考:P2/P3 非阻断,可留债归档;要修——只碰治理文件(.sillyspec/**,不动本变更 requirements/design)的提交不触发重评;触及交付面或 requirements/design 需重评(复审只验修复+新增,非全量首评)`)
1599
1614
  }
1600
1615
  console.log(`✅ 独立评审通过(reviewer=${v.review.reviewer}${tier.sampled ? ',豁免抽查采样命中' : ''})`)
1601
1616
  reviewOutcome = { required: true, sampled: tier.sampled, verdict: 'PASS', findingsP1: 0 }
@@ -1684,7 +1699,7 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1684
1699
  try {
1685
1700
  const { harvestSlot4Decision } = await import('./flow-parity.js')
1686
1701
  const h = harvestSlot4Decision({ changeDir, change })
1687
- if (h.harvested) console.log(`🌱 槽4(风险与死路)收割 → decisions.md(随蒸馏链进 knowledge)`)
1702
+ if (h.harvested) console.log(`🌱 槽4(风险与死路)收割 → decisions.md(底稿——作答里的放弃方案请拆成 rejected 条目再收口;随蒸馏链进 knowledge)`)
1688
1703
  } catch { /* 收割 best-effort */ }
1689
1704
  try {
1690
1705
  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
  }