sillyspec 3.33.4 → 3.33.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.
@@ -0,0 +1,48 @@
1
+ ---
2
+ name: sillyspec:config
3
+ description: 用于 SillySpec local.yaml 配置引导。适合用户说"配置下 sillyspec、连接平台、配 MCP、lint 不跑、测试太慢、加豁免"或 agent 需要写 .sillyspec/local.yaml 时。核心原则:默认零配置可用,只写真需要的键,写完必须 config validate 通过。
4
+ ---
5
+
6
+ ## 何时使用
7
+
8
+ - 用户要求配置 SillySpec(平台凭据/MCP/测试/lint/调优键)
9
+ - agent 判断需要写 `.sillyspec/local.yaml`(任何场景——本技能是唯一引导面)
10
+
11
+ ## 第一原则:默认零配置可用(先读这段再动手)
12
+
13
+ **local.yaml 一个键都不写,SillySpec 就是完整可用的。** 全部 ~70 个注册键中只有平台/MCP 凭据族(4 键)真必填,且仅当用户要用平台功能。测试面按变更动态推断、lint 未配置自动跳过归 pre-push、FR 回归层缺省不跑归 CI——都是零配置最优缺省(2026-10-23 起)。
14
+
15
+ **不要「预防性配置」**:不确定要不要写的键就是不写。写了多余键的唯一效果是偏离缺省、增加审计面。
16
+
17
+ ## 必填场景决策表(只有这些场景才动配置)
18
+
19
+ | 场景 | 症状/触发 | 要写的键 | 怎么获得值 |
20
+ |---|---|---|---|
21
+ | 平台同步/看板 | 用户要用 SillyHub 平台 | `platform.url` + `platform.token` | **不手填**:跑 `sillyspec platform connect <url> --token <t>` 自动写 |
22
+ | MCP 派发 | 评审平台单调用/独立审查派发 | `mcp.url` + `mcp.token` | 同上(connect 成对签发) |
23
+ | 评审单调用 | 用户要评审收口一次完成(省 missing→PASS 往返) | `review_inline_wait: 180` | 依赖 mcp 凭据;平台不可用自动回退任务书面零损失 |
24
+ | dirty 门残渣 | 收口反复被已知残渣文件拦(--accept-dirty-gap 成常态) | `dirty_gate_ignore: <前缀1>, <前缀2>` | 看收口输出点名的文件,取其稳定目录前缀 |
25
+ | lint 收口实测 | 用户要 lint 在收口时跑(缺省跳过归 pre-push) | `commands.lint: <命令>` | 从 package.json scripts / Makefile 抄 |
26
+ | 慢套件超时 | 实测被 600s 默认帽判超时失败 | `commands.test_timeout_sec: 2400` | 收口输出会提示 |
27
+ | 全量测试道 | 用户明确要本地跑全量(罕见) | `test_strategy: full` | 仅显式全量时;缺省动态子集已是最优 |
28
+ | 项目特有评审必查面 | 团队有自己的评审红线 | `review_probes:` 段 | 见 `sillyspec config schema` review_probes 节 |
29
+
30
+ 调优键全清单与语义:`sillyspec config schema`(单一真相;example 列可照抄值)。
31
+
32
+ ## 操作流程
33
+
34
+ 1. **看现状**:`sillyspec config cat`(权威路径链解析——worktree 内也能拿到主仓真实文件;不存在=零配置形态,正常)。
35
+ 2. **对决策表**:确认触发场景真实存在;不在表内的场景**不写任何键**,向用户说明缺省行为已覆盖。
36
+ 3. **获得值**:凭据族走 `sillyspec platform connect`(禁止手填 token 到 yaml);其余键从仓库结构(package.json/pyproject/Makefile)或收口输出提示取值。
37
+ 4. **写文件**:编辑 `.sillyspec/local.yaml`(config cat 显示的路径;整文件 YAML,注意缩进用空格、保存为 UTF-8 无 BOM——PS 下用 Set-Content -Encoding utf8NoBOM 或让 CLI 命令代写)。**只写决策表需要的键,逐键照抄 schema 的 example 形态。**
38
+ 5. **硬门验证(必做,不许跳过)**:跑 `sillyspec config validate`
39
+ - **exit 0 才算配置完成**;exit 1 时按输出的错误行逐条修复后重跑。
40
+ - 有「未知顶层键」警告=键名/层级写错,回 schema 核对。
41
+ - 凭据段报缺键=两键要成对(或整段删除回退)。
42
+ - **禁止**在 validate 未通过时向用户宣称配置完成。
43
+
44
+ ## 边界
45
+
46
+ - 禁止写 `.sillyspec/` 之外的 local.yaml(hook 拦截);项目根没有这个文件。
47
+ - 禁止把 token 写进 local.yaml.example 或任何可提交文件(真实 local.yaml 是 gitignored)。
48
+ - known_failures 豁免键不用手改 yaml:`sillyspec config known-failures add|remove`(行级手术保注释)。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sillyspec",
3
- "version": "3.33.4",
3
+ "version": "3.33.5",
4
4
  "description": "SillySpec CLI — 流程状态机,让 AI 严格按步骤来",
5
5
  "icon": "logo.jpg",
6
6
  "homepage": "https://sillyspec.ppdmq.top/",
@@ -122,6 +122,15 @@ export const LOCAL_YAML_SCHEMA = {
122
122
  keys: [
123
123
  { path: 'modules.<name>.path', type: 'string', optional: true, status: 'live', readers: ['extractModules (src/verify-postcheck.js)', 'parseLocalYamlModules (src/plan-postcheck.js)', 'extractModulePaths (src/worktree-deps.js)'], desc: '子模块目录路径(相对仓库根)。', example: 'frontend/' },
124
124
  { path: 'modules.<name>.test', type: 'string', optional: true, status: 'deprecated', readers: ['extractModules (src/verify-postcheck.js,退役提示用)'], desc: '【2026-09-26-dynamic-test-inference 退役,不再消费】测试面按变更动态推断、runner 自项目结构(pyproject/uv.lock/package.json 就近祖先)推断。模块级 flaky 处置迁 known_failures。块在场时 CLI 打印退役指引。', example: 'cd frontend && pnpm test' },
125
+ { path: 'fr_unmapped_baseline', type: 'integer', optional: true, status: 'live', readers: ['unmapped 大池消音(src/fr-index.js)'], desc: 'unmapped 域 FR 条数消音基线(跨仓历史堆积已清算时设当前值;增量 >10 才警告,缺省 0=无基线恒警告行为)。', example: '720' },
126
+ ],
127
+ },
128
+ {
129
+ id: 'repos',
130
+ title: '跨仓注册表',
131
+ note: '仓键 → 本地工作目录。注册后参与跨仓冻结面/对账/未标注交付探测与跨仓 worktree 落位;条目两种形态:字符串路径或 {path, worktree} 对象(worktree=跨仓 worktree 落位根,优先级见 worktree.crossPlacement)。local.yaml 本就 gitignored,真实本地路径不入库。',
132
+ keys: [
133
+ { path: 'repos.<repo键>', type: 'string', optional: true, status: 'live', readers: ['跨仓冻结/对账(src/cross-repo-reconcile.js)、注册探测(src/local-register.js)、跨仓 worktree(src/worktree-cross.js)'], desc: '仓键 → 该仓本地工作目录绝对路径(或 {path, worktree} 对象形态)。', example: 'shared-lib: C:/path/to/shared-lib' },
125
134
  ],
126
135
  },
127
136
  {
@@ -131,6 +140,7 @@ export const LOCAL_YAML_SCHEMA = {
131
140
  keys: [
132
141
  { path: 'verify.test_rerun', type: 'enum', values: ['incremental', 'full'], optional: true, status: 'live', readers: ['readTestRerunConfig (src/verify-postcheck.js — dynamic-subset 分支增量档)'], desc: '实测门失败后重跑档:incremental(缺省)= 前轮失败后,下轮只跑「失败批测试文件 ∪ 自失败基线以来变更文件」的三源推断面(未触碰绿面复用,返工轮分钟级);full = 恒全子集重跑(2026-10-07 前现状,保守项目用)。env SILLYSPEC_TEST_RERUN 优先。仅作用 dynamic-subset 档(显式 test_strategy: full 不受影响)。', example: 'verify: { test_rerun: full }' },
133
142
  { path: 'test_strategy', type: 'enum', values: ['full', 'module', 'skip', 'evidence-auto'], optional: true, status: 'live', readers: ['extractTestStrategy (src/verify-postcheck.js)', 'resolveTestStrategy (src/verify-postcheck.js)'], desc: '【2026-09-26-dynamic-test-inference 起】full=全量(commands.test 在场生效,否则结构推断);module=已退役折算动态子集(超集覆盖:变更文件面收窄 + FR 关联回归 + import 依赖);skip=真跳过(R-07 不变);evidence-auto=按 module-impact.md 推荐(行为→动态子集、文档/门禁→skip)。缺省=动态子集:三源并集非空实测、空则不硬跑全量(防超时/预存失败面)。', example: 'full' },
143
+ { path: 'fr_regress_cap', type: 'string', optional: true, status: 'live', readers: ['readFrRegressCap (src/verify-postcheck.js)'], desc: '【2026-10-23-fr-regress-default-off 缺省翻转】FR 关联回归层本地实测开关:缺省(不写)=不跑(归 pre-push/CI 门,收口披露行留痕);N(1-2000)=前 N 个绑定文件进实测面;all=全跑旧行为显式恢复;off=同缺省。', example: '40' },
134
144
  ],
135
145
  },
136
146
  {
@@ -188,6 +198,12 @@ export const LOCAL_YAML_SCHEMA = {
188
198
  { path: 'flow.mode', type: 'enum', values: ['thin', 'legacy'], optional: true, status: 'live', readers: ['readFlowConfig (src/flow.js)'], desc: '2-调用轻量协议开关:thin(缺省——2026-09-25-thin-default-flip 翻转,入口归一:常规变更默认轻量跑道,需求不清晰 CLI 拦下指路头脑风暴预段,实测失败自动升厚)= flow start/done 轻量跑道;legacy = 显式回旧道(run <stage> 全族行为逐字不动,flow start 拒跑并指路)。切回一行 yaml。', example: 'thin' },
189
199
  { path: 'flow.edit_ratio_threshold', type: 'number', optional: true, status: 'live', readers: ['readFlowConfig (src/flow.js)'], desc: '机器稿改写比例路由阈值(amend 通道计算的行级 editRatio 超阈 → 厚档提示 route_hint)。缺省 0.5。', example: '0.5' },
190
200
  { path: 'flow.edit_ratio_enforcement', type: 'enum', values: ['advisory', 'block'], optional: true, status: 'live', readers: ['readFlowConfig (src/flow.js)'], desc: 'editRatio 超阈的执行档:advisory(缺省——测绿可轻量档过,只提示+遥测)| block(超阈阻断 flow done)。用户裁定 advisory 定案,dogfood 后可按遥测翻 block。', example: 'advisory' },
201
+ // 以下四键为根级标量(无点 path——2026-10-23-config-validate-skill 补登:此前散落
202
+ // 各 reader 内联正则、schema 发现面缺失;校验器按注册面跟随)
203
+ { path: 'turbo', type: 'enum', values: ['docs-auto'], optional: true, status: 'live', readers: ['readFlowConfig (src/flow.js)'], desc: '【2026-10-23-turbo-docs-auto】纯文档面常设极速授权档:docs-auto=CLI 按提交∪脏面自动判纯文档(代码分类器同源)降档 turbo(实测/评审/UI 探针跳过,授权源留痕);不写=关(--turbo 旗标档不受影响,agent 仍不可自启)。', example: 'docs-auto' },
204
+ { path: 'review_inline_wait', type: 'integer', optional: true, status: 'live', readers: ['readFlowConfig (src/flow.js)'], desc: '【2026-10-23-review-inline-dispatch】薄道评审平台单调用通道等待预算秒数(1-600,钳 300s 帽):>0 时 flow done 评审缺席分支经 SillyHub 派发 readOnly worker 有界轮询回收、同调用登记(missing→PASS 往返消灭);任何失败回退任务书面零损失;不写/坏值=关。', example: '180' },
205
+ { path: 'dirty_gate_ignore', type: 'string', optional: true, status: 'live', readers: ['readDirtyGateIgnore (src/flow-parity.js)'], desc: '【2026-10-23-dirty-gate-ignore】dirty 冻结缺口门的已知残渣前缀忽略清单(单行逗号分隔、posix 前缀匹配,module_gate_ignore 同款):命中面从缺口剔除并每轮 ℹ️ 留痕;三态 fail-soft 未配置=现状。', example: 'quicklog, test/.sillyspec' },
206
+ { path: 'module_gate_ignore', type: 'string', optional: true, status: 'live', readers: ['readFlowConfig (src/flow.js)'], desc: '【2026-10-12-thin-module-gate-and-feed】模块图未登记目录软门的固定忽略清单(单行逗号、目录前缀、posix)——合法未登记面(根级脚本/配置等)免登记签收。', example: 'scripts, .github' },
191
207
  ],
192
208
  },
193
209
  {
@@ -426,6 +442,18 @@ dispatch:
426
442
  # mode: thin # thin=flow start/done 轻量跑道(协议必需交互=2)| legacy=既有 run <stage> 全族(回滚一行)
427
443
  # edit_ratio_threshold: 0.5 # 机器稿改写比例路由阈值(超阈提示厚档;缺省 0.5)
428
444
  # edit_ratio_enforcement: advisory # advisory=测绿可轻量过只提示(缺省)| block=超阈阻断
445
+ #
446
+ # ── 收口调优(根级键,缺省零配置即用——注释态=缺省已最优,确要调优再解注释)──
447
+ # turbo: docs-auto # 纯文档面自动极速档(实测/评审/UI 探针跳过,授权源留痕);不写=关
448
+ # review_inline_wait: 180 # 评审平台单调用等待秒(1-600,需 SillyHub);不写=评审任务书面模式
449
+ # dirty_gate_ignore: quicklog, test/.sillyspec # dirty 门已知残渣前缀(单行逗号、posix 前缀)
450
+ # module_gate_ignore: scripts # 模块图软门固定忽略目录(单行逗号、前缀匹配)
451
+ # fr_regress_cap: 40 # FR 回归层本地开关:缺省不跑归 pre-push/CI 门;N=前 N 个;all=全跑旧行为
452
+ # fr_unmapped_baseline: 720 # unmapped 域 FR 消音基线(跨仓历史堆积已清算时设)
453
+ #
454
+ # ── 跨仓注册表(注册后参与跨仓冻结/对账;local.yaml gitignored 故真实路径不入库)──
455
+ # repos:
456
+ # shared-lib: C:/path/to/shared-lib # 或对象形态 {path: …, worktree: <跨仓落位根>}
429
457
 
430
458
  # ── 阶段说明书下发与会话墙(R16 减负批次)──
431
459
  # stage:
@@ -0,0 +1,180 @@
1
+ /**
2
+ * config-validate.js — local.yaml 逐键校验器(2026-10-23-config-validate-skill)
3
+ *
4
+ * 用户配置哲学(2026-10-23 裁决原话):「能不要配置的就别让用户配置,默认都集成好;实在
5
+ * 不好配置的要生成一个技能去引导 agent 怎么填写,并且 agent 写完要 CLI 验证通过才行」。
6
+ * 本模块是那句「CLI 验证」:sillyspec config validate 经 config-cat 权威路径链解析真实
7
+ * local.yaml(worktree 感知),按 LOCAL_YAML_SCHEMA 单一源逐键校验——校验只消费不生产
8
+ * (schema 注册面新增键照常跟随,无需改本模块)。
9
+ *
10
+ * 判级:错误(exit 1)=YAML 解析失败/顶层非映射/段非映射/枚举越界/结构性类型错(标量位
11
+ * 收到映射或数组)/不可强转标量(number/integer 键收 "abc" 类纯文本、boolean 键收非
12
+ * true|false 串)/段内必填键缺失(凭据族两键齐全才生效)。警告(exit 0)=未知键(可能
13
+ * 无效果)。info(exit 0)=deprecated 键在场/空文件。标量强转语义(评审 P1 修正):数字
14
+ * 串与数字同义("2400"≡2400——YAML 弱型等价),但不可强转的错拼类必拦——防「宽容」变成
15
+ * 放过 FR 承诺要拦的坏值。缺文件=零配置可用 info exit 0(69 键仅 4 凭据键真必填且仅平台
16
+ * 用户——零配置契约锚)。
17
+ */
18
+ import yaml from 'js-yaml'
19
+ import { readFileSync } from 'fs'
20
+ import { LOCAL_YAML_SCHEMA } from './config-schema.js'
21
+ import { resolveLocalYaml } from './config-cat.js'
22
+
23
+ /** schema 注册面索引:path→def、已知顶层键(段 id ∪ 无点 path)、含占位段的段(免未知键误报)。 */
24
+ function buildRegistry() {
25
+ const byPath = new Map()
26
+ const sectionIds = new Set()
27
+ const rootScalars = new Set()
28
+ const placeholderSections = new Set()
29
+ for (const sec of LOCAL_YAML_SCHEMA.sections || []) {
30
+ sectionIds.add(sec.id)
31
+ for (const k of sec.keys || []) {
32
+ byPath.set(k.path, k)
33
+ if (!k.path.includes('.')) rootScalars.add(k.path)
34
+ if (/<[^>]+>/.test(k.path)) placeholderSections.add(sec.id)
35
+ }
36
+ }
37
+ return { byPath, sectionIds, knownTop: new Set([...sectionIds, ...rootScalars]), placeholderSections }
38
+ }
39
+
40
+ const kindOf = (v) => (v === null || v === undefined ? 'null' : Array.isArray(v) ? 'array' : typeof v)
41
+
42
+ /** 类型检查(评审 P1 修正口径:可强转宽容、不可强转判错)——YAML 标量弱型下 24 与 "24"
43
+ * 同义(number/integer 收数字串合法),但 "abc" 这类不可强转错拼必须拦(FR-01 场景);
44
+ * boolean 收非 true/false 串判错;string 收任意标量;结构错配(标量位收映射/数组)恒错。 */
45
+ function checkType(def, v, path, errors) {
46
+ const kind = kindOf(v)
47
+ if (kind === 'null') return
48
+ const scalarSpot = ['string', 'number', 'integer', 'enum', 'boolean', 'json'].includes(def.type)
49
+ if (scalarSpot && (kind === 'object' || kind === 'array')) {
50
+ errors.push(`「${path}」期望${def.type === 'enum' ? `枚举(${(def.values || []).join('|')})` : def.type}标量,实得 ${kind}——嵌套层级写错(该键是叶子值不是段)`)
51
+ return
52
+ }
53
+ if (def.type === 'object' && kind !== 'object') {
54
+ errors.push(`「${path}」期望键值映射,实得 ${kind}`)
55
+ return
56
+ }
57
+ if (def.type === 'array' && kind !== 'array') {
58
+ errors.push(`「${path}」期望列表,实得 ${kind}`)
59
+ return
60
+ }
61
+ if (def.type === 'enum' && !(def.values || []).map(String).includes(String(v))) {
62
+ errors.push(`「${path}」取值越界:${JSON.stringify(v)}(合法:${(def.values || []).join('|')})`)
63
+ return
64
+ }
65
+ if ((def.type === 'number' || def.type === 'integer') && kind === 'string' && !/^-?\d+(\.\d+)?$/.test(v.trim())) {
66
+ errors.push(`「${path}」期望${def.type === 'integer' ? '整数' : '数字'},实得字符串 ${JSON.stringify(v)}(不可强转——数字串可,纯文本不可)`)
67
+ return
68
+ }
69
+ if (def.type === 'integer' && kind === 'string' && !/^-?\d+$/.test(v.trim())) {
70
+ errors.push(`「${path}」期望整数,实得 ${JSON.stringify(v)}`)
71
+ return
72
+ }
73
+ if (def.type === 'boolean' && kind === 'string' && !/^(true|false)$/i.test(v.trim())) {
74
+ errors.push(`「${path}」期望布尔(true/false),实得 ${JSON.stringify(v)}`)
75
+ }
76
+ }
77
+
78
+ /**
79
+ * 校验 local.yaml 文本(纯函数)。@returns {{errors:string[], warnings:string[], infos:string[], keysChecked:number}}
80
+ */
81
+ export function validateLocalYamlContent(text) {
82
+ const { byPath, knownTop, placeholderSections } = buildRegistry()
83
+ const errors = [], warnings = [], infos = []
84
+ let doc
85
+ try { doc = yaml.load(text) } catch (e) {
86
+ return { errors: [`YAML 解析失败:${(e && e.message) || e}——agent 手改易犯缩进/Tab/编码错(doctor 的 local_yaml_integrity 维度查编码)`], warnings, infos, keysChecked: 0 }
87
+ }
88
+ if (doc == null) doc = {}
89
+ const topKind = kindOf(doc)
90
+ if (topKind !== 'object') {
91
+ return { errors: [`local.yaml 顶层必须是键值映射(实得 ${topKind})`], warnings, infos, keysChecked: 0 }
92
+ }
93
+ const sectionIds = new Set((LOCAL_YAML_SCHEMA.sections || []).map((s) => s.id))
94
+ let checked = 0
95
+ const checkDef = (def, v, path) => {
96
+ checked++
97
+ checkType(def, v, path, errors)
98
+ if (def.status === 'deprecated') infos.push(`「${path}」已退役(${String(def.desc || '').slice(0, 80)}…)——新配置勿再写`)
99
+ }
100
+ for (const [k, v] of Object.entries(doc)) {
101
+ if (!knownTop.has(k)) {
102
+ warnings.push(`未知顶层键「${k}」——sillyspec config schema 可查全部已知键(未知键大概率无效果;确认拼写与层级)`)
103
+ continue
104
+ }
105
+ if (sectionIds.has(k) && byPath.has(k) === false) {
106
+ // 纯段(如 project/commands):校验段内键。两种真实形态:键值映射(commands.lint)
107
+ // 与列表-of-对象(review_probes 列表项字段建模为 sec.field 点路径——本仓 dogfood 实证)。
108
+ if (v == null) continue
109
+ const vKind = kindOf(v)
110
+ if (vKind !== 'object' && vKind !== 'array') { errors.push(`段「${k}」必须是键值映射或列表(实得 ${vKind})`); continue }
111
+ const entries = vKind === 'array'
112
+ ? (v.every((it) => kindOf(it) === 'object') ? v.flatMap((it) => Object.entries(it)) : Object.entries(v[0] || {}))
113
+ : Object.entries(v)
114
+ for (const [ik, iv] of entries) {
115
+ const path = `${k}.${ik}`
116
+ const def = byPath.get(path)
117
+ if (!def) {
118
+ // 占位段(modules.<name>.path 族):子键名是用户自定义 id,注册面无法穷举——
119
+ // 未知子键不警告(占位形态本身合法),具体叶子键校验交给占位 path 前缀匹配不到即跳过
120
+ if (!placeholderSections.has(k)) warnings.push(`「${path}」不在注册键表(可能拼错/层级错,或新版键未同步 schema——sillyspec config schema 核对)`)
121
+ continue
122
+ }
123
+ checkDef(def, iv, path)
124
+ }
125
+ } else {
126
+ // 根级标量键(test_strategy/module_gate_ignore/…)
127
+ const def = byPath.get(k)
128
+ if (!def) { warnings.push(`键「${k}」不在注册键表`); continue }
129
+ checkDef(def, v, k)
130
+ }
131
+ }
132
+ // 段内必填闭合(required:true 键所在段在场则须齐全——mcp/platform 凭据族两键成对才生效)
133
+ for (const sec of LOCAL_YAML_SCHEMA.sections || []) {
134
+ if (!(sec.id in doc) || doc[sec.id] == null || kindOf(doc[sec.id]) !== 'object') continue
135
+ for (const kd of sec.keys || []) {
136
+ if (kd.required !== true || !kd.path.startsWith(`${sec.id}.`)) continue
137
+ const leaf = kd.path.slice(sec.id.length + 1)
138
+ if (!(leaf in doc[sec.id])) {
139
+ errors.push(`段「${sec.id}」缺必填键「${leaf}」——${sec.id === 'mcp' || sec.id === 'platform' ? '凭据两键齐全才生效(缺半整段回退缺省/env);补齐或整段删除' : '必填键'}`)
140
+ }
141
+ }
142
+ }
143
+ if (checked === 0 && errors.length === 0) infos.push('local.yaml 在场但无任何已注册键(空段/仅注释)——等于零配置形态')
144
+ return { errors, warnings, infos, keysChecked: checked }
145
+ }
146
+
147
+ /**
148
+ * config validate 子命令体:路径链解析(config-cat 同源,worktree 感知)→ 校验 → 人类/--json
149
+ * 双态渲染。退出码:0=通过(可含警告/info),1=有错误;缺文件=零配置 info exit 0。
150
+ */
151
+ export function cmdConfigValidate({ dir, specBase = null, json = false } = {}) {
152
+ const r = resolveLocalYaml(dir, { specBase })
153
+ if (!r.exists) {
154
+ const payload = { file: null, exists: false, passed: true, errors: [], warnings: [], infos: ['local.yaml 不存在——零配置可用(全部键走缺省;仅平台/MCP 凭据族真必填)'], keysChecked: 0 }
155
+ if (json) console.log(JSON.stringify(payload, null, 2))
156
+ else console.log('✅ local.yaml 不存在——零配置可用(全部键走缺省;仅平台/MCP 凭据族真必填,需要时 sillyspec platform connect 一键写入)。\n 需要调优时:sillyspec config schema 查键 + sillyspec-config 技能引导;写完跑 sillyspec config validate 验证。')
157
+ return 0
158
+ }
159
+ let v
160
+ try { v = validateLocalYamlContent(readFileSync(r.path, 'utf8')) } catch (e) {
161
+ // 读盘竞态兜底(评审 P3 清偿):resolve 与 read 之间文件消失/被锁——优雅报错不裸崩
162
+ if (json) console.log(JSON.stringify({ file: r.path, exists: true, passed: false, errors: [`local.yaml 读取失败:${(e && e.message) || e}(可能刚被移动/删除——重跑)`], warnings: [], infos: [], keysChecked: 0 }, null, 2))
163
+ else console.error(`❌ local.yaml 读取失败:${(e && e.message) || e}(可能刚被移动/删除——重跑 sillyspec config validate)`)
164
+ return 1
165
+ }
166
+ const passed = v.errors.length === 0
167
+ if (json) {
168
+ console.log(JSON.stringify({ file: r.path, exists: true, passed, ...v }, null, 2))
169
+ } else {
170
+ console.log(`${passed ? '✅' : '❌'} local.yaml 校验${passed ? '通过' : '未通过'}(${r.path}${r.source ? `,来源 ${r.source}` : ''};已查 ${v.keysChecked} 个注册键)`)
171
+ for (const e of v.errors) console.error(` 错误:${e}`)
172
+ for (const w of v.warnings) console.warn(` 警告:${w}`)
173
+ for (const i of v.infos) console.log(` ℹ️ ${i}`)
174
+ if (passed && v.warnings.length === 0 && v.errors.length === 0) console.log(' 无警告无未知键。')
175
+ if (!passed) console.error(' 修复后重跑:sillyspec config validate(exit 0 才算配置完成——sillyspec-config 技能硬门)')
176
+ }
177
+ return passed ? 0 : 1
178
+ }
179
+
180
+ export default { validateLocalYamlContent, cmdConfigValidate }
@@ -522,6 +522,20 @@ export function detectPatchDrift({ cwd, change, freezeHead }) {
522
522
  * gate-snapshot 同款判定注入)时未提交 dirty 交付面一并入冻结——独占树内 dirty 全归属本变更;
523
523
  * 共享主仓无法归属 → 不入面只警告(他侧声明的 dirty 连警告都免)。
524
524
  */
525
+ /**
526
+ * dirty_gate_ignore 读取(2026-10-23-dirty-gate-ignore,module_gate_ignore/readFlowConfig
527
+ * 同款逐键正则防线):local.yaml 单行逗号串(目录/文件前缀,posix),列表块形态不支持。
528
+ * 三态 fail-soft——未配置/键值非法/local.yaml 不可读 → 空清单(dirty 门行为与现状逐字一致)。
529
+ */
530
+ export function readDirtyGateIgnore(specBase) {
531
+ try {
532
+ const raw = readFileSync(join(specBase, 'local.yaml'), 'utf8')
533
+ const m = raw.match(/^\s*dirty_gate_ignore\s*:\s*(.+)$/m)
534
+ if (!m) return []
535
+ return m[1].split(',').map((s) => s.trim().replace(/\\/g, '/')).filter(Boolean)
536
+ } catch { return [] }
537
+ }
538
+
525
539
  export function collectFreezeFiles({ cwd, specBase, change, committed, exclusive }) {
526
540
  const deliverable = (f) => !String(f).replace(/\\/g, '/').startsWith('.sillyspec/')
527
541
  && !isToolGovernancePath(f)
@@ -540,6 +554,12 @@ export function collectFreezeFiles({ cwd, specBase, change, committed, exclusive
540
554
  if (deliverable(path) && !dirty.includes(path)) dirty.push(path)
541
555
  }
542
556
  } catch { /* git 失败零 dirty 面 */ }
557
+ // 已知残渣前缀剔除(2026-10-23-dirty-gate-ignore,单一源):仓主显式声明的残渣面
558
+ // (quicklog/、测试夹具运行库等)不进 dirty 冻结缺口——剔除进 dirtyIgnored 随返回值
559
+ // 留证(权威门打 ℹ️ 留痕行);预检(flow-preflight.js)经本函数自动同源继承。
560
+ const ignorePrefixes = readDirtyGateIgnore(specBase)
561
+ const dirtyIgnored = ignorePrefixes.length > 0 ? dirty.filter((f) => ignorePrefixes.some((pfx) => f.startsWith(pfx))) : []
562
+ if (dirtyIgnored.length > 0) dirty = dirty.filter((f) => !dirtyIgnored.includes(f))
543
563
  // 归属切分单点:exclusive(worktree/--freeze-dirty)与共享主仓共用——他侧已声明 dirty 永不入冻
544
564
  // (评审 P2 修复:--freeze-dirty 承诺「非他侧声明」,全量冻入会造成跨变更审计双计;worktree
545
565
  // 下 own 恒为全量,切分无伤)
@@ -547,8 +567,8 @@ export function collectFreezeFiles({ cwd, specBase, change, committed, exclusive
547
567
  try {
548
568
  ownDirty = splitOwnVsForeignDiffFiles(cwd, change, dirty, { specBase }).own
549
569
  } catch { /* 归属切分失败:exclusive 保留全量(声明即边界),共享主仓退全量警告 */ }
550
- if (exclusive) return { files: [...new Set([...committed, ...ownDirty])], dirtyAdded: ownDirty, dirtyWarned: [] }
551
- return { files: [...committed], dirtyAdded: [], dirtyWarned: ownDirty }
570
+ if (exclusive) return { files: [...new Set([...committed, ...ownDirty])], dirtyAdded: ownDirty, dirtyWarned: [], dirtyIgnored }
571
+ return { files: [...committed], dirtyAdded: [], dirtyWarned: ownDirty, dirtyIgnored }
552
572
  }
553
573
 
554
574
  /**
@@ -0,0 +1,67 @@
1
+ /**
2
+ * flow-preflight.js — 收口预检(fail-together 一次报全,2026-10-23-flow-done-preflight)
3
+ *
4
+ * 背景(收口画像实证):621 次 flow done 调用/255 变更=2.44 次/变更,净浪费 152 次 RTT(24.5%)
5
+ * 源于逐门 fail-fast 串行拒绝;dirty-gap 门在实测门之后——56% 触发率的门每次拦截都先付一轮
6
+ * 完整实测(58s 起、大仓分钟级)。
7
+ *
8
+ * 本模块是纯前移报告层:实测门前只读收集「独立可算的便宜阻断条件」,由 flow.js 侧渲染一次
9
+ * 报全后拦在付费实测之前。判定与权威门同源同判定(复用同一批函数而非重写),旗标与守卫
10
+ * 逐条镜像 patch 子步:
11
+ * - dirty-gap 预判镜像 collectFreezeFiles dirtyWarned 非空 && !freezeDirty && !acceptDirtyGap
12
+ * && !turbo(turbo 下权威门降 advisory,镜像不拦)
13
+ * - orphan-face 预判镜像仅会话独占 worktree(shouldSkipGateSnapshotForWorktree)且
14
+ * !acceptOrphanFace
15
+ * - 守卫:baseline 缺席跳过(patch 子步整体跳过面);patch 子步已 done 跳过——预检绝不
16
+ * 重复拦已过权威门的面(断点续不误拦)
17
+ *
18
+ * 放弃项(评审已裁决,见 design 风险与死路):review.json schema 预验不做——schema 违规的
19
+ * 成本是「修正文件+重跑」一轮 RTT,与检测时点无关(同轮提前检测不省轮次);且豁免轮
20
+ * (--no-review)权威门不读文件,预检先验会多拦权威门不拦的面(判定漂移),三路皆负。
21
+ *
22
+ * fail-safe 单向:预检异常静默放行(返回空 findings)——漏报由权威门兜底(仍在原子步原序),
23
+ * 预检不构成新失败面;全程零写盘(不落冻结件、不 mark 子步、不写 flow-state)。
24
+ */
25
+ import { gitQuiet } from './git-helper.js'
26
+
27
+ /**
28
+ * 收集收口预检阻断条件(纯计算零打印)。
29
+ * @returns {{ findings: Array<{id:'dirty-gap',count:number,files:string[]} | {id:'orphan-face',commits:string[],count:number,files:string[]}> }}
30
+ */
31
+ export async function collectCloseoutPreflight({ cwd, specBase, change, st, freezeDirty = false, acceptDirtyGap = false, acceptOrphanFace = false, turbo = false }) {
32
+ const findings = []
33
+ const patchDone = st?.substeps?.patch === 'done'
34
+ // ── dirty-gap / orphan-face 预判(镜像 patch 子步判定链)──
35
+ if (!patchDone && st?.baseline_commit) {
36
+ try {
37
+ let exclusiveFrom = freezeDirty === true ? 'flag' : null
38
+ if (exclusiveFrom == null) {
39
+ try {
40
+ const { shouldSkipGateSnapshotForWorktree } = await import('./run/gate-snapshot.js')
41
+ if (shouldSkipGateSnapshotForWorktree(cwd)) exclusiveFrom = 'worktree'
42
+ } catch { /* 判定异常按共享主仓(权威门同款) */ }
43
+ }
44
+ const exclusive = exclusiveFrom != null
45
+ const committedRaw = String(gitQuiet(cwd, ['diff', '--name-only', `${st.baseline_commit}..HEAD`]) || '')
46
+ .split('\n').map((s) => s.trim().replace(/\\/g, '/')).filter(Boolean)
47
+ const { filterCommittedFace, collectFreezeFiles, scanOrphanFace } = await import('./flow-parity.js')
48
+ const ownPrefix = `.sillyspec/changes/${change}/`
49
+ const committed = filterCommittedFace(committedRaw, ownPrefix)
50
+ const freeze = collectFreezeFiles({ cwd, specBase, change, committed, exclusive })
51
+ if (freeze.dirtyWarned.length > 0 && !freezeDirty && !acceptDirtyGap && !turbo) {
52
+ findings.push({ id: 'dirty-gap', count: freeze.dirtyWarned.length, files: freeze.dirtyWarned })
53
+ }
54
+ if (exclusiveFrom === 'worktree' && !acceptOrphanFace) {
55
+ try {
56
+ const orphan = scanOrphanFace(cwd, { baseline: st.baseline_commit })
57
+ if (orphan.files.length > 0) {
58
+ findings.push({ id: 'orphan-face', commits: orphan.commits, count: orphan.files.length, files: orphan.files })
59
+ }
60
+ } catch { /* orphan 预判 fail-safe(权威门同款不阻断) */ }
61
+ }
62
+ } catch { /* 预检 fail-safe:异常静默放行(权威门仍在 patch 子步原序原判定) */ }
63
+ }
64
+ return { findings }
65
+ }
66
+
67
+ export default { collectCloseoutPreflight }
package/src/flow.js CHANGED
@@ -83,14 +83,22 @@ export function readFlowConfig(specBase) {
83
83
  // module_gate_ignore(2026-10-12-thin-module-gate-and-feed):未登记目录软门的固定忽略清单,
84
84
  // 单行逗号串(目录前缀,posix)——列表块形态不支持,readFlowConfig 同款逐键正则防线
85
85
  const ig = raw.match(/^\s*module_gate_ignore\s*:\s*(.+)$/m)
86
+ // turbo docs-auto 常设授权档(2026-10-23-turbo-docs-auto):仓主一行声明纯文档面自动降档;
87
+ // 逐键正则同款防线,未配置/坏值(turbo: off 等)fail-soft 恒 false
88
+ const turboDocsAuto = /^\s*turbo\s*:\s*docs-auto\s*$/m.test(raw)
89
+ // review_inline_wait(2026-10-23-review-inline-dispatch):薄道评审平台单调用通道的等待
90
+ // 预算秒数(1-600 正整数有效;缺省/坏值 0=off——回退现状任务书面零回归)
91
+ const riw = raw.match(/^\s*review_inline_wait\s*:\s*([0-9]+)\s*$/m)
86
92
  return {
87
93
  mode,
88
94
  editRatioThreshold: th ? Number(th[1]) : 0.5,
89
95
  editRatioEnforcement: en ? en[1] : 'advisory',
90
96
  moduleGateIgnore: ig ? ig[1].split(',').map((s) => s.trim()).filter(Boolean) : [],
97
+ turboDocsAuto,
98
+ reviewInlineWaitSeconds: riw ? Math.min(600, Math.max(0, Number(riw[1]))) : 0,
91
99
  }
92
100
  } catch {
93
- return { mode: 'thin', editRatioThreshold: 0.5, editRatioEnforcement: 'advisory', moduleGateIgnore: [] }
101
+ return { mode: 'thin', editRatioThreshold: 0.5, editRatioEnforcement: 'advisory', moduleGateIgnore: [], turboDocsAuto: false, reviewInlineWaitSeconds: 0 }
94
102
  }
95
103
  }
96
104
 
@@ -630,7 +638,7 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
630
638
  const _localYaml = join(specBase, 'local.yaml')
631
639
  if (!existsSync(_localYaml) && !existsSync(join(specBase, 'changes', change))) {
632
640
  console.error('❌ local.yaml 不存在——先跑 sillyspec init(mode/lint/known_failures 等 init 级配置的落点)。')
633
- console.error(' 测试面无需在此配置(2026-09-26 起:测试门按变更动态推断——本变更测试 ∪ FR 关联回归 ∪ import 依赖,')
641
+ console.error(' 测试面无需在此配置(测试门按变更动态推断——本变更测试 ∪ import 依赖;FR 关联回归层缺省不跑归 pre-push/CI 门(2026-10-23-fr-regress-default-off),fr_regress_cap: N|all 可开启,')
634
642
  console.error(' runner 自项目结构 pyproject/package.json 推断;modules.*.test/commands.test 已退役,后者仅显式 test_strategy: full 生效)。')
635
643
  process.exit(2)
636
644
  }
@@ -1158,6 +1166,7 @@ export async function cmdFlowStart({ change, input, title: titleFlag = null, thi
1158
1166
  ``,
1159
1167
  `🛑 三断点纪律(顺序纪律:①未过不动码;②③为汇报点——用户没说「全跑完」就必须在每个断点向用户汇报并等确认):`,
1160
1168
  ` ① spec 断点【机器门】:填完 FR 区和 design 四节后,把摘要给用户看(FR 条目+盲维作答+方案概述),`,
1169
+ ` 起草完先自查:sillyspec flow verify-drafts --change ${change}(与批准门同判定——自查通过再请批准,用户一次过);`,
1161
1170
  ` 用户确认后请其运行 sillyspec flow approve --change ${change}(批准留痕)再动手写代码——`,
1162
1171
  ` agent 代跑时如实声明通道:--user-confirmed(转述用户已确认)/ --self-approved(agent 自批),`,
1163
1172
  ` 无 flag 按 unattested 留痕(审计面无法区分用户确认与自批)——2026-10-12-approve-channel-attest。`,
@@ -1268,7 +1277,35 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1268
1277
  }
1269
1278
  // 极速模式读态(2026-10-14-thin-turbo-mode FR-02/D-007):turbo && tier!=='thick' 双条件——
1270
1279
  // 升厚(distill 异态/verify 失败/--thick 起步)后 turbo 残留不短路厚档仪式,重入即恢复全量校验
1271
- const turbo = st.turbo === true && st.tier !== 'thick'
1280
+ // turbo 双源(2026-10-23-turbo-docs-auto):旗标档(--turbo 用户显式)原样;未开且仓主常设
1281
+ // 授权 turbo: docs-auto 时按面自动降档——面=changedFilesSinceBaseline(提交∪脏面)经
1282
+ // dirty_gate_ignore 前缀剔除(与 dirty 门同一残渣口径)后非空且无文件命中代码分类器
1283
+ // (quick-audit 单一源)。fail-closed:面空/含代码/读取或判定异常一律走全量(不静默降档);
1284
+ // 升厚即失效对双源同效(tier=thick 关)。授权权在仓主(local.yaml 一行),agent 仍无法自选。
1285
+ const turboFlag = st.turbo === true && st.tier !== 'thick'
1286
+ let turbo = turboFlag
1287
+ let turboSource = turboFlag ? 'flag' : null
1288
+ if (!turbo && st.tier !== 'thick') {
1289
+ try {
1290
+ if (readFlowConfig(specBase).turboDocsAuto === true) {
1291
+ const { readDirtyGateIgnore } = await import('./flow-parity.js')
1292
+ const { CODE_PATH_SEGMENT_RE, CODE_EXTENSION_RE } = await import('./run/quick-audit.js')
1293
+ const ignorePfx = readDirtyGateIgnore(specBase)
1294
+ const face = (changedFilesSinceBaseline(cwd, st.baseline_commit) || [])
1295
+ .filter((f) => !ignorePfx.some((p) => f.startsWith(p)))
1296
+ const docsOnly = face.length > 0 && !face.some((f) => CODE_PATH_SEGMENT_RE.test(f) || CODE_EXTENSION_RE.test(f))
1297
+ if (docsOnly) {
1298
+ turbo = true
1299
+ turboSource = 'docs-auto'
1300
+ console.log(`⚡ turbo(docs-auto):提交/冻结面纯文档(${face.length} 文件)——实测/评审/UI 探针按极速档跳过(授权源 local.yaml turbo: docs-auto;升厚即失效)`)
1301
+ }
1302
+ }
1303
+ } catch { /* 面判定异常 fail-closed 走全量 */ }
1304
+ }
1305
+ // 实测门 reason 的授权源标签(防文案漂移:docs-auto 档不得显示「用户显式触发」)
1306
+ const turboLabel = turboSource === 'docs-auto'
1307
+ ? 'local.yaml turbo: docs-auto 常设授权(提交/冻结面纯文档)'
1308
+ : `用户显式触发 ${st.turbo_since || '时点未记'}`
1272
1309
  const runtimeRoot = resolveRuntimeRoot(runtimeRootOpt ? { runtimeRoot: runtimeRootOpt } : {}, specBase)
1273
1310
  // 归属收窄清单(2026-09-25-thin-dogfood-fixes 修复②):baseline..HEAD 不分作者——并行会话在
1274
1311
  // start..done 之间的提交会混入本变更实测面与 FR 域路由。复用 verify 对账同款切分器:他侧显式
@@ -1377,7 +1414,7 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1377
1414
  if (dr.applicable && dr.emptySlots.length > 0) {
1378
1415
  console.error(`❌ 设计记录未作答:design.md 有 ${dr.emptySlots.length} 个空 AGENT 槽(${dr.emptySlots.join('、')})`)
1379
1416
  console.error(` 每节至少写一行(小改动可写「不适用:<理由>」)——设计承诺是评审与 FR 对账的锚点,空槽=承诺未落盘`)
1380
- console.error(` 恢复指引:槽标记(<!--AGENT:槽N)被删时,从 .runtime/step-guides/ 的指纹缓存可找骨架原文;或删 design.md 后重入 flow start 补生成(criteria 从 proposal 回提)`)
1417
+ console.error(` 恢复指引:槽标记(<!--AGENT:槽N)被删时,骨架原文永存于 .runtime/state/draft-ledger-${change}.json 的 files["design.md"].text(draft-ledger 机器稿首版永存——2026-10-23 实证修正:step-guides/ 只有阶段指引,从未缓存 flow design 骨架,旧指引指向不存在的缓存);从 ledger 取槽标记与问题行原样补回(作答放问题行之后——槽在问题行前会被当成答案);或删 design.md 后重入 flow start 补生成(criteria 从 proposal 回提,注意会重置为骨架态)`)
1381
1418
  reportMidFail('artifacts')
1382
1419
  process.exit(1)
1383
1420
  }
@@ -1578,13 +1615,49 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1578
1615
  }
1579
1616
  }
1580
1617
  } catch { /* 节奏门 best-effort:读流失败静默(fail-open——观测缺席不阻断) */ }
1618
+ // ── 收口预检(fail-together 一次报全,2026-10-23-flow-done-preflight):实测门前只读收集
1619
+ // 便宜阻断条件(dirty-gap/orphan 预判)——56% 触发率的 dirty 门不再每轮先付完整实测;
1620
+ // 命中即拦在付费实测之前,全部条件+处置出路一次报全(b1ab65b2「两条修复一次给全」
1621
+ // 模式升格)。全过静默零行为变化;判定与权威门同源同旗标镜像(collectCloseoutPreflight
1622
+ // fail-safe:异常静默放行,权威门仍在原子步原序)。
1623
+ {
1624
+ const { collectCloseoutPreflight } = await import('./flow-preflight.js')
1625
+ const pre = await collectCloseoutPreflight({ cwd, specBase, change, st, freezeDirty, acceptDirtyGap, acceptOrphanFace, turbo })
1626
+ if (pre.findings.length > 0) {
1627
+ console.error(`⛔ 收口预检未过——${pre.findings.length} 项阻断条件(fail-together 一次报全;实测未启动,处置后重跑同一条命令从断点续):`)
1628
+ for (const f of pre.findings) {
1629
+ if (f.id === 'dirty-gap') {
1630
+ // 逐字复用 patch 子步权威门文案(防两路口径漂移——文案即契约面)
1631
+ console.error(` ▸ ${f.count} 个未提交交付文件未入冻结面(共享主仓无法归属)——三选一:`)
1632
+ console.error(`⛔ 未提交交付缺口未显式处置——收口阻断(不归档;处置后重跑同一条命令从断点续):`)
1633
+ console.error(` ① git commit 这些文件(信息尾缀带 (${change}) 或 (${change})),重跑本命令——冻结面自动并入`)
1634
+ console.error(` ② 全部归属本变更:flow done --change ${change} --freeze-dirty`)
1635
+ console.error(` ③ 接受审计缺口:flow done --change ${change} --accept-dirty-gap`)
1636
+ console.error(` 💡 快速通道:脏文件全属并行会话在途/运行残渣且不在本变更交付面时,直接重跑带 --accept-dirty-gap(显式 pathspec 提交面已隔离归属;属本变更的文件必须走①提交)`)
1637
+ console.error(` ④ 已知残渣固定忽略:.sillyspec/local.yaml 加 dirty_gate_ignore: <目录前缀>(单行逗号分隔,同 module_gate_ignore)`)
1638
+ console.error(` ${f.files.slice(0, 3).join('、')}${f.files.length > 3 ? ' 等' : ''}`)
1639
+ } else if (f.id === 'orphan-face') {
1640
+ // 逐字复用 patch 子步权威门文案(防两路口径漂移——文案即契约面)
1641
+ console.error(` ▸ 会话专属 worktree 冻结面外交付改动——三选一:`)
1642
+ console.error(`⛔ 会话专属 worktree 存在冻结面外交付改动:${f.commits.length} 个提交不在 HEAD 祖先链(patch-id 不等价)触及 ${f.count} 个交付文件——收口阻断(不归档;处置后重跑同一条命令从断点续):`)
1643
+ console.error(` 提交:${f.commits.slice(0, 5).map((c) => c.slice(0, 10)).join('、')}${f.commits.length > 5 ? ' 等' : ''}`)
1644
+ console.error(` 文件:${f.files.slice(0, 5).join('、')}${f.files.length > 5 ? ' 等' : ''}`)
1645
+ console.error(` ① git rebase/cherry-pick 这些提交到 HEAD(补齐冻结面)后重跑`)
1646
+ console.error(` ② 确认放弃这些提交:清理对应分支/reflog 源后重跑`)
1647
+ console.error(` ③ 接受缺口留痕:flow done --change ${change} --accept-orphan-face`)
1648
+ }
1649
+ }
1650
+ reportMidFail('patch')
1651
+ process.exit(1)
1652
+ }
1653
+ }
1581
1654
  // 极速模式实测门跳过(2026-10-14-thin-turbo-mode FR-02/D-002):turbo 在场不调
1582
1655
  // runQuickTestLintGate,合成 gate 同形状 skipped 对象(带 status:'skipped' 完整形态——
1583
1656
  // 下方 fmt 对 null 显示 '—',带形态才输出「skipped — 跳过原因」,审计可见;
1584
1657
  // 对齐 quick-audit env 逃生门 skip 形态先例 src/run/quick-audit.js:482)。
1585
1658
  // 失败升厚分支(下方 action==='fail')turbo 下天然不触达——升厚语义不变。
1586
1659
  const gate = turbo
1587
- ? { action: 'skip', failed: [], reason: `⚡ 极速模式(用户显式触发 ${st.turbo_since || '时点未记'})——test+lint 实测跳过(审计留痕;本次收口不含测试客观核验)`, test: { status: 'skipped', command: null, exitCode: null, durationMs: null, outputTail: null, reason: `⚡ 极速模式(用户显式触发 ${st.turbo_since || '时点未记'})——本次收口不含测试客观核验(审计留痕)`, resultPath: null }, lint: { status: 'skipped', command: null, exitCode: null, durationMs: null, outputTail: null, reason: '⚡ 极速模式——lint 实测跳过(审计留痕)', resultPath: null } }
1660
+ ? { action: 'skip', failed: [], reason: `⚡ 极速模式(${turboLabel})——test+lint 实测跳过(审计留痕;本次收口不含测试客观核验)`, test: { status: 'skipped', command: null, exitCode: null, durationMs: null, outputTail: null, reason: `⚡ 极速模式(${turboLabel})——本次收口不含测试客观核验(审计留痕)`, resultPath: null }, lint: { status: 'skipped', command: null, exitCode: null, durationMs: null, outputTail: null, reason: '⚡ 极速模式——lint 实测跳过(审计留痕)', resultPath: null } }
1588
1661
  : await runQuickTestLintGate({ cwd, specBase, changedFiles, changeName: change, skipSentinel: true /* flow 侧哨兵带 baseline 区间,quick 侧区间不可靠——单判不双判 */ })
1589
1662
  if (gate && gate.action === 'fail') {
1590
1663
  console.error(`❌ 测试门 FAIL(整单 FAIL——实测失败/超时=失败,不继续 distill/归档):`)
@@ -1902,6 +1975,11 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1902
1975
  if (freeze.dirtyAdded.length > 0) {
1903
1976
  console.log(`🔒 ${exclusiveFrom === 'flag' ? '--freeze-dirty 显式声明' : '会话专属 worktree'}:${freeze.dirtyAdded.length} 个未提交交付文件一并入冻结面`)
1904
1977
  }
1978
+ // 已知残渣剔除留痕(2026-10-23-dirty-gate-ignore):collectFreezeFiles 单一源剔除
1979
+ // dirty_gate_ignore 命中面——每轮一行可审计(配了什么、剔了几个),预检同源继承。
1980
+ if (Array.isArray(freeze.dirtyIgnored) && freeze.dirtyIgnored.length > 0) {
1981
+ console.log(`ℹ️ ${freeze.dirtyIgnored.length} 个 dirty 文件按 local.yaml dirty_gate_ignore 剔除(已知残渣面,不入冻结缺口):${freeze.dirtyIgnored.slice(0, 3).join('、')}${freeze.dirtyIgnored.length > 3 ? ' 等' : ''}`)
1982
+ }
1905
1983
  if (freeze.dirtyWarned.length > 0) {
1906
1984
  console.warn(`⚠️ ${freeze.dirtyWarned.length} 个未提交交付文件未入冻结面(共享主仓无法归属)——三选一:`)
1907
1985
  console.warn(` ① 先提交这些文件(信息尾缀带变更名)后重跑 ② 确认全部归属本变更:重跑 flow done --freeze-dirty 并入冻结 ③ 确认接受审计缺口:重跑 flow done --accept-dirty-gap(缺口数随 change-patch.json 留痕)。Git 中间提交可归档后压扁(见收口指引)`)
@@ -1922,6 +2000,7 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
1922
2000
  console.error(` ② 全部归属本变更:flow done --change ${change} --freeze-dirty`)
1923
2001
  console.error(` ③ 接受审计缺口:flow done --change ${change} --accept-dirty-gap`)
1924
2002
  console.error(` 💡 快速通道:脏文件全属并行会话在途/运行残渣且不在本变更交付面时,直接重跑带 --accept-dirty-gap(显式 pathspec 提交面已隔离归属;属本变更的文件必须走①提交)`)
2003
+ console.error(` ④ 已知残渣固定忽略:.sillyspec/local.yaml 加 dirty_gate_ignore: <目录前缀>(单行逗号分隔,同 module_gate_ignore)`)
1925
2004
  reportMidFail('patch')
1926
2005
  process.exit(1)
1927
2006
  }
@@ -2351,6 +2430,41 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
2351
2430
  } catch { /* 无/坏 JSON → 保持 null(既有判定) */ }
2352
2431
  }
2353
2432
  const tier = classifyReviewNeed({ changeDir, input: null, patchText, editRatio: st.edit_ratio ?? null, reviewForce: st.review_force ?? null, change })
2433
+ // 登记块单一源(2026-10-23-review-inline-dispatch):子代理重跑路径与平台 inline 回收
2434
+ // 路径同源消费——validateReviewJson+FAIL/P1 拦截+P2/P3 留债+PASS mark,无第二套判定。
2435
+ const registerReviewOutcome = async () => {
2436
+ const v = validateReviewJson(join(changeDir, 'review.json'))
2437
+ if (!v.ok) {
2438
+ console.error(`❌ review.json 校验失败(${v.errors.join(';')})——按任务书 schema 修正后重跑`)
2439
+ reviewOutcome = { required: true, sampled: tier.sampled, verdict: 'invalid' }
2440
+ appendTelemetry(reviewOutcome) // 修评审 P2①
2441
+ reportMidFail('review')
2442
+ process.exit(1)
2443
+ }
2444
+ const p1s = (v.review.findings || []).filter((f) => /^P1$/i.test(String(f.severity || '')))
2445
+ const others = (v.review.findings || []).filter((f) => !/^P1$/i.test(String(f.severity || '')))
2446
+ if (v.review.verdict === 'FAIL' || p1s.length > 0) {
2447
+ console.error(`❌ 独立评审未过(verdict=${v.review.verdict},P1 发现 ${p1s.length} 项)——承诺与实现不一致,不许归档:`)
2448
+ for (const f of [...p1s, ...others]) console.error(` [${f.severity}] ${f.title} — ${f.evidence || ''}${f.location ? `(${f.location})` : ''}`)
2449
+ console.error(` 修复后删除 review.json 并重新执行评审任务书(起子代理重评),再重跑 flow done(断点续)`)
2450
+ reviewOutcome = { required: true, sampled: tier.sampled, verdict: 'FAIL', findingsP1: p1s.length }
2451
+ appendTelemetry(reviewOutcome) // 修评审 P2①:sampled+FAIL 恰是误豁免率校准的最关键信号
2452
+ reportMidFail('review')
2453
+ process.exit(1)
2454
+ }
2455
+ if (others.length > 0) {
2456
+ console.warn(`⚠️ 评审非阻断发现 ${others.length} 项(P2/P3,随归档留档):`)
2457
+ for (const f of others) console.warn(` [${f.severity}] ${f.title} — ${f.evidence || ''}`)
2458
+ // 处置提示(2026-10-10-drift-review-governance-keep,坑 drift-review-governance-loop):
2459
+ // P2/P3 非阻断可留债归档;要修则修复面决定是否重评——只碰治理文件不动本变更
2460
+ // requirements/design 的后缀提交触发治理面等价保留(评审保留+自动重冻结,零重评);
2461
+ // 触及交付面或承诺面才隔离重评(任务书带前轮 findings,复审只验修复+新增)。
2462
+ console.warn(` 💡 处置参考:P2/P3 非阻断,可留债归档;要修——只碰治理文件(.sillyspec/**,不动本变更 requirements/design)的提交不触发重评;触及交付面或 requirements/design 需重评(复审只验修复+新增,非全量首评)`)
2463
+ }
2464
+ console.log(`✅ 独立评审通过(reviewer=${v.review.reviewer}${tier.sampled ? ',豁免抽查采样命中' : ''})`)
2465
+ reviewOutcome = { required: true, sampled: tier.sampled, verdict: 'PASS', findingsP1: 0 }
2466
+ mark('review')
2467
+ }
2354
2468
  if (!tier.required) {
2355
2469
  console.log(`⚖️ 评审豁免(低风险证据齐全):${tier.exemptEvidence.join(';')}`)
2356
2470
  reviewOutcome = { required: false, sampled: false, verdict: 'exempt' }
@@ -2361,9 +2475,17 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
2361
2475
  // 不再被漂移重冻结误隔离(实测竞态修复)。rev-parse 失败传 null(任务书退引导形态)。
2362
2476
  let taskbookHead = null
2363
2477
  try { const h = gitQuiet(cwd, ['rev-parse', 'HEAD']); if (h && /^[0-9a-f]{7,40}$/i.test(String(h).trim())) taskbookHead = String(h).trim() } catch { /* best-effort */ }
2364
- // 派发纪律(2026-10-19-thin-review-dispatch-verbatim):横幅是主会话派发时最先读到的通道,
2365
- // 明确「原样复制」——实测手写缩编会丢材料包(厚道 stage-review.js:803 同款指令的薄道对齐)。
2366
- console.log(`⚖️ 本变更需要独立评审(${tier.reasons.join(';')})——评审任务书如下,任务书原样复制给子代理(勿自行改写增删)执行后重跑本命令:\n`)
2478
+ // 平台单调用通道(2026-10-23-review-inline-dispatch):review_inline_wait 键门控——键有效
2479
+ // 先试 inline(派发→有界等待→回收→同调用登记,missing→PASS 往返消灭);任何失败回退
2480
+ // 现状任务书面(零治理损失)。键 off 时本分支与改动前逐字一致。
2481
+ const inlineWaitSec = readFlowConfig(specBase).reviewInlineWaitSeconds
2482
+ if (inlineWaitSec > 0) {
2483
+ console.log(`⚖️ 本变更需要独立评审(${tier.reasons.join(';')})——平台单调用通道已启用(review_inline_wait=${inlineWaitSec}s),先试 inline 派发:`)
2484
+ } else {
2485
+ // 派发纪律(2026-10-19-thin-review-dispatch-verbatim):横幅是主会话派发时最先读到的通道,
2486
+ // 明确「原样复制」——实测手写缩编会丢材料包(厚道 stage-review.js:803 同款指令的薄道对齐)。
2487
+ console.log(`⚖️ 本变更需要独立评审(${tier.reasons.join(';')})——评审任务书如下,任务书原样复制给子代理(勿自行改写增删)执行后重跑本命令:\n`)
2488
+ }
2367
2489
  // 项目评审探针装配(2026-10-12-review-probe-injection):local.yaml 数据驱动的项目特有
2368
2490
  // 必查面注入任务书(未配置零注入;装载告警/帽留痕 best-effort 提示,不阻断渲染)。
2369
2491
  let _probeLines = null
@@ -2403,43 +2525,39 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
2403
2525
  const _pack = assembleReviewPack({ changeDir, patchText, attribution: _attribution, testReceipt: _receipt })
2404
2526
  if (_pack && Array.isArray(_pack.lines) && _pack.lines.length > 0) _packLines = _pack.lines
2405
2527
  } catch { /* 材料包面 fail-soft:装配失败零注入 */ }
2406
- console.log(renderReviewerTaskbook({ change, changeDir, head: taskbookHead, probeLines: _probeLines, packLines: _packLines }))
2407
- reviewOutcome = { required: true, sampled: tier.sampled, verdict: 'missing' }
2408
- appendTelemetry(reviewOutcome) // 修评审 P2①:失败面先落账再 exit(校准信号不丢)
2409
- reportMidFail('review')
2410
- process.exit(1)
2411
- } else {
2412
- const v = validateReviewJson(join(changeDir, 'review.json'))
2413
- if (!v.ok) {
2414
- console.error(`❌ review.json 校验失败(${v.errors.join(';')})——按任务书 schema 修正后重跑`)
2415
- reviewOutcome = { required: true, sampled: tier.sampled, verdict: 'invalid' }
2416
- appendTelemetry(reviewOutcome) // 修评审 P2①
2417
- reportMidFail('review')
2418
- process.exit(1)
2528
+ const taskbookText = renderReviewerTaskbook({ change, changeDir, head: taskbookHead, probeLines: _probeLines, packLines: _packLines })
2529
+ let inlineWritten = false
2530
+ if (inlineWaitSec > 0) {
2531
+ try {
2532
+ const { runThinReviewInline } = await import('./review-dispatch.js')
2533
+ const inline = await runThinReviewInline({
2534
+ cwd, specBase, change, changeDir, taskbookText,
2535
+ waitMs: Math.min(300_000, inlineWaitSec * 1000),
2536
+ log: (m) => console.log(m),
2537
+ })
2538
+ if (inline.ok) {
2539
+ console.log(`✅ 平台评审单调用回收(mission ${String(inline.missionId).slice(0, 8)}/worker ${String(inline.workerId).slice(0, 8)},reviewer=platform-inline)——同调用登记结论(missing→PASS 往返消灭)`)
2540
+ inlineWritten = true
2541
+ } else {
2542
+ console.log(`ℹ️ 平台评审单调用未回收:${inline.reason}——回退评审任务书面`)
2543
+ }
2544
+ } catch (e) {
2545
+ console.log(`ℹ️ 平台评审单调用异常(${(e && e.message) || e})——回退评审任务书面`)
2546
+ }
2419
2547
  }
2420
- const p1s = (v.review.findings || []).filter((f) => /^P1$/i.test(String(f.severity || '')))
2421
- const others = (v.review.findings || []).filter((f) => !/^P1$/i.test(String(f.severity || '')))
2422
- if (v.review.verdict === 'FAIL' || p1s.length > 0) {
2423
- console.error(`❌ 独立评审未过(verdict=${v.review.verdict},P1 发现 ${p1s.length} 项)——承诺与实现不一致,不许归档:`)
2424
- for (const f of [...p1s, ...others]) console.error(` [${f.severity}] ${f.title} — ${f.evidence || ''}${f.location ? `(${f.location})` : ''}`)
2425
- console.error(` 修复后删除 review.json 并重新执行评审任务书(起子代理重评),再重跑 flow done(断点续)`)
2426
- reviewOutcome = { required: true, sampled: tier.sampled, verdict: 'FAIL', findingsP1: p1s.length }
2427
- appendTelemetry(reviewOutcome) // 修评审 P2①:sampled+FAIL 恰是误豁免率校准的最关键信号
2548
+ if (!inlineWritten) {
2549
+ if (inlineWaitSec > 0) {
2550
+ console.log(`⚖️ 回退评审任务书面——任务书原样复制给子代理(勿自行改写增删)执行后重跑本命令:\n`)
2551
+ }
2552
+ console.log(taskbookText)
2553
+ reviewOutcome = { required: true, sampled: tier.sampled, verdict: 'missing' }
2554
+ appendTelemetry(reviewOutcome) // 修评审 P2①:失败面先落账再 exit(校准信号不丢)
2428
2555
  reportMidFail('review')
2429
2556
  process.exit(1)
2430
2557
  }
2431
- if (others.length > 0) {
2432
- console.warn(`⚠️ 评审非阻断发现 ${others.length} 项(P2/P3,随归档留档):`)
2433
- for (const f of others) console.warn(` [${f.severity}] ${f.title} — ${f.evidence || ''}`)
2434
- // 处置提示(2026-10-10-drift-review-governance-keep,坑 drift-review-governance-loop):
2435
- // P2/P3 非阻断可留债归档;要修则修复面决定是否重评——只碰治理文件不动本变更
2436
- // requirements/design 的后缀提交触发治理面等价保留(评审保留+自动重冻结,零重评);
2437
- // 触及交付面或承诺面才隔离重评(任务书带前轮 findings,复审只验修复+新增)。
2438
- console.warn(` 💡 处置参考:P2/P3 非阻断,可留债归档;要修——只碰治理文件(.sillyspec/**,不动本变更 requirements/design)的提交不触发重评;触及交付面或 requirements/design 需重评(复审只验修复+新增,非全量首评)`)
2439
- }
2440
- console.log(`✅ 独立评审通过(reviewer=${v.review.reviewer}${tier.sampled ? ',豁免抽查采样命中' : ''})`)
2441
- reviewOutcome = { required: true, sampled: tier.sampled, verdict: 'PASS', findingsP1: 0 }
2442
- mark('review')
2558
+ await registerReviewOutcome()
2559
+ } else {
2560
+ await registerReviewOutcome()
2443
2561
  }
2444
2562
  }
2445
2563
 
@@ -2712,6 +2830,82 @@ export async function cmdFlowDone({ change, cwd, specBase, runtimeRootOpt = null
2712
2830
  }
2713
2831
 
2714
2832
  /** flow 命令族入口(index.js case 'flow' 接线):flow start|done|amend-draft。 */
2833
+ /**
2834
+ * spec 面完整性只读校验单一源(2026-10-23-flow-verify-drafts):approve 批准门与
2835
+ * verify-drafts 自查命令同判定同源——verifyFlowDrafts repair:false 只读轨(不做 design 问题行
2836
+ * 自动回填,写盘修复属 done 面)+ ledger 机本位缺失降级(仅 v1 AGENT 槽形态跑空槽兜底)。
2837
+ * 纯计算零打印:调用方各自渲染(approve 文案钉死既有形态,verify-drafts 按新面渲染)。
2838
+ * 返回 { applicable, schema, violations, advisories, skipDesign, degraded, v1EmptySlots }。
2839
+ */
2840
+ async function computeSpecFaceChecks({ changeDir, change, specBase, runtimeRootOpt = null }) {
2841
+ const runtimeRoot = resolveRuntimeRoot(runtimeRootOpt ? { runtimeRoot: runtimeRootOpt } : {}, specBase)
2842
+ const { verifyFlowDrafts, verifyDesignRecordFilled } = await import('./flow-draft.js')
2843
+ const st = readFlowState(changeDir) || {}
2844
+ const adoptedDesign = st.adopted_from === 'brainstorm' && existsSync(join(changeDir, 'design.md'))
2845
+ const r = verifyFlowDrafts({ changeDir, change, runtimeRoot, skipDesign: adoptedDesign, repair: false })
2846
+ const out = {
2847
+ applicable: r.applicable === true, schema: r.schema || null,
2848
+ violations: r.violations || [], advisories: r.advisories || [],
2849
+ skipDesign: adoptedDesign, degraded: false, v1EmptySlots: [],
2850
+ }
2851
+ if (!r.applicable) {
2852
+ // draft-ledger 机本位缺失(跨机/存量变更):fail-open 降级——仅 v1(AGENT 槽形态)跑空槽
2853
+ // 兜底;v2 纯 markdown 无 ledger 无法对锚校验,如实降级(不假装能查)。
2854
+ out.degraded = true
2855
+ let v1Text = null
2856
+ try { v1Text = readFileSync(join(changeDir, 'design.md'), 'utf8') } catch { /* 无 design */ }
2857
+ if (v1Text && /<!--AGENT:槽\d+/.test(v1Text)) {
2858
+ const dr = verifyDesignRecordFilled({ changeDir })
2859
+ if (dr.applicable) out.v1EmptySlots = dr.emptySlots
2860
+ }
2861
+ }
2862
+ return out
2863
+ }
2864
+
2865
+ /**
2866
+ * flow verify-drafts(2026-10-23-flow-verify-drafts):spec 面起草自查只读命令——起草完
2867
+ * 先自查(与批准门同判定同源 computeSpecFaceChecks)、通过再请用户批准,把「用户跑 approve
2868
+ * 被 exit 1 硬拒」的往返消灭在 agent 侧。零写盘、零状态副作用(通过≠批准——批准仍走 approve
2869
+ * 留痕)。--json 与人类渲染同判定同退出码(对齐 flow status --json 双态同源先例)。
2870
+ */
2871
+ async function cmdFlowVerifyDrafts({ change, cwd, specBase, runtimeRootOpt = null, json = false }) {
2872
+ const changeDir = join(specBase, 'changes', change)
2873
+ if (!existsSync(changeDir)) {
2874
+ const msg = `❌ 变更不存在:${change}(flow verify-drafts 在 flow start 之后、approve 之前运行——起草面自查)`
2875
+ if (json) console.log(JSON.stringify({ change, status: 'missing' }))
2876
+ else console.error(msg)
2877
+ process.exit(2)
2878
+ }
2879
+ const r = await computeSpecFaceChecks({ changeDir, change, specBase, runtimeRootOpt })
2880
+ const hasViolations = r.applicable && r.violations.length > 0
2881
+ const hasEmptySlots = r.degraded && r.v1EmptySlots.length > 0
2882
+ const passed = !hasViolations && !hasEmptySlots
2883
+ if (json) {
2884
+ console.log(JSON.stringify({
2885
+ change, passed, applicable: r.applicable, schema: r.schema,
2886
+ violations: r.violations, advisories: r.advisories,
2887
+ skipDesign: r.skipDesign, degraded: r.degraded, v1EmptySlots: r.v1EmptySlots,
2888
+ }))
2889
+ if (!passed) process.exit(1) // 与人类渲染同退出码(json 不分叉)
2890
+ } else if (passed) {
2891
+ for (const a of r.advisories) console.warn(`⚠️ [门柱漂移 advisory] ${a}(不阻断批准)`)
2892
+ if (r.degraded) console.log(`ℹ️ 自查降级:draft-ledger 不在此机(.runtime 机本位/存量变更)——空槽兜底校验通过(v2 纯 markdown 无法对锚校验,如实降级)`)
2893
+ console.log(`✅ spec 面自查通过(0 violations)——通过≠批准:请把摘要给用户看,用户确认后 sillyspec flow approve --change ${change}(批准留痕后动码)`)
2894
+ } else {
2895
+ for (const a of r.advisories) console.warn(`⚠️ [门柱漂移 advisory] ${a}(不阻断批准)`)
2896
+ if (hasViolations) {
2897
+ console.error(`❌ spec 面自查未通过——${r.violations.length} 处违规(与 flow approve 批准门同判定;一次全列,修复后重跑本命令):`)
2898
+ for (const v of r.violations) console.error(` - ${v}`)
2899
+ }
2900
+ if (hasEmptySlots) {
2901
+ console.error(`❌ spec 面自查未通过——design.md 有 ${r.v1EmptySlots.length} 个空 AGENT 槽(${r.v1EmptySlots.join('、')}):每节至少一行(「不适用:<理由>」也算答)`)
2902
+ }
2903
+ console.error(` 修复后重跑:sillyspec flow verify-drafts --change ${change}——通过再请用户批准(flow approve),用户一次过`)
2904
+ process.exit(1)
2905
+ }
2906
+ return { change, passed }
2907
+ }
2908
+
2715
2909
  /**
2716
2910
  * flow approve(2026-10-04-thin-docs-v2 FR-06):spec 断点用户批准留痕——用户确认 FR+design
2717
2911
  * 方案后运行;写 flow-state {spec_approved,spec_approved_at,spec_approved_by}。flow done 的
@@ -2747,11 +2941,10 @@ async function cmdFlowApprove({ change, cwd, specBase, by = null, channelFlag =
2747
2941
  // 面只读,不做 design 问题行自动回填(写盘修复属 done 面)。诚实边界:本门证明「答在批前」,
2748
2942
  // 不证明「答在码前」——时序证据路线已实证不可行(批量收口下 git 时间戳说谎,thin-order-canon
2749
2943
  // 四路调查),不做假装能拦的检查。
2944
+ // 计算单一源 computeSpecFaceChecks(2026-10-23-flow-verify-drafts 抽取共享)——本块只保留
2945
+ // approve 面的渲染与退出,文案与退出码逐字保持(既有测试零改动作回归锚)。
2750
2946
  {
2751
- const runtimeRoot = resolveRuntimeRoot({}, specBase)
2752
- const { verifyFlowDrafts, verifyDesignRecordFilled } = await import('./flow-draft.js')
2753
- const adoptedDesign = st.adopted_from === 'brainstorm' && existsSync(join(changeDir, 'design.md'))
2754
- const r = verifyFlowDrafts({ changeDir, change, runtimeRoot, skipDesign: adoptedDesign, repair: false })
2947
+ const r = await computeSpecFaceChecks({ changeDir, change, specBase })
2755
2948
  for (const a of r.advisories || []) console.warn(`⚠️ [门柱漂移 advisory] ${a}(不阻断批准)`)
2756
2949
  if (r.applicable && r.violations.length > 0) {
2757
2950
  console.error(`❌ spec 断点未批准——文档未齐(批准是动码前置,先答 spec 面再批):`)
@@ -2762,15 +2955,10 @@ async function cmdFlowApprove({ change, cwd, specBase, by = null, channelFlag =
2762
2955
  if (!r.applicable) {
2763
2956
  // draft-ledger 机本位缺失(跨机批准/存量变更):fail-open 降级——仅 v1(AGENT 槽形态)跑
2764
2957
  // 空槽兜底;v2 纯 markdown 无 ledger 无法对锚校验,如实降级放行(不假装能查)。
2765
- let v1Text = null
2766
- try { v1Text = readFileSync(join(changeDir, 'design.md'), 'utf8') } catch { /* 无 design */ }
2767
- if (v1Text && /<!--AGENT:槽\d+/.test(v1Text)) {
2768
- const dr = verifyDesignRecordFilled({ changeDir })
2769
- if (dr.applicable && dr.emptySlots.length > 0) {
2770
- console.error(`❌ spec 断点未批准——design.md 有 ${dr.emptySlots.length} 个空 AGENT 槽(${dr.emptySlots.join('、')}):每节至少一行(「不适用:<理由>」也算答)`)
2771
- console.error(` 作答齐备后重跑:sillyspec flow approve --change ${change} [--user-confirmed|--self-approved]`)
2772
- process.exit(1)
2773
- }
2958
+ if (r.v1EmptySlots.length > 0) {
2959
+ console.error(`❌ spec 断点未批准——design.md 有 ${r.v1EmptySlots.length} 个空 AGENT 槽(${r.v1EmptySlots.join('、')}):每节至少一行(「不适用:<理由>」也算答)`)
2960
+ console.error(` 作答齐备后重跑:sillyspec flow approve --change ${change} [--user-confirmed|--self-approved]`)
2961
+ process.exit(1)
2774
2962
  }
2775
2963
  console.log(`ℹ️ 批准校验降级放行:draft-ledger 不在此机(.runtime 机本位/存量变更)——空槽兜底校验通过`)
2776
2964
  }
@@ -2962,6 +3150,15 @@ export async function cmdFlow(args, cwd, specDir = null, opts = {}) {
2962
3150
  await reportAgentLog(change, sub)
2963
3151
  return cmdFlowReject({ change, cwd, specBase, reason: getFlag('--reason') })
2964
3152
  }
3153
+ if (sub === 'verify-drafts') {
3154
+ // spec 面起草自查只读面(2026-10-23-flow-verify-drafts):与批准门同判定同源——起草完先
3155
+ // 自查、通过再请批准,用户被拒往返消灭在 agent 侧;零写盘零状态副作用。
3156
+ const change = getFlag('--change')
3157
+ if (!change) { console.error('❌ flow verify-drafts 需 --change <名>'); process.exit(2) }
3158
+ validateChangeName(change)
3159
+ await reportAgentLog(change, sub)
3160
+ return cmdFlowVerifyDrafts({ change, cwd, specBase, runtimeRootOpt, json: jsonGlobal || hasFlag('--json') })
3161
+ }
2965
3162
  if (sub === 'diff') {
2966
3163
  // 只读查询面(2026-10-13-flow-diff-cmd):change 粒度文件统计双态口——与 status 同级自愿面
2967
3164
  const change = getFlag('--change')
@@ -3152,7 +3349,7 @@ export async function cmdFlow(args, cwd, specDir = null, opts = {}) {
3152
3349
  console.log(`✅ flow amend-draft 留痕重锚:${r.reanchored.join('、')}——ledger amendment 审计在案;editRatio=${maxRatio}(段级最大;基准=首版原文,AGENT 槽不计入)`)
3153
3350
  return r
3154
3351
  }
3155
- console.error('用法: sillyspec flow start --change <名> --input "<一句 10-30 字中文概括(首行即变更标题,硬区间 10-30 必须、建议 ~20 字)+动机与背景详述+独立一行『成功标准:』+每行一条『- 可验证标准』>" [--thick|--with-tasks] [--no-prestage(hindsight 标记在场时声明需求已含决策,留痕放行)] | sillyspec flow done --change <名> | sillyspec flow status --change <名> [--json] | sillyspec flow diff --change <名> [--json](只读文件统计)')
3352
+ console.error('用法: sillyspec flow start --change <名> --input "<一句 10-30 字中文概括(首行即变更标题,硬区间 10-30 必须、建议 ~20 字)+动机与背景详述+独立一行『成功标准:』+每行一条『- 可验证标准』>" [--thick|--with-tasks] [--no-prestage(hindsight 标记在场时声明需求已含决策,留痕放行)] | sillyspec flow done --change <名> | sillyspec flow status --change <名> [--json] | sillyspec flow verify-drafts --change <名> [--json](spec 面只读自查,通过再请批准) | sillyspec flow diff --change <名> [--json](只读文件统计)')
3156
3353
  console.error(' --input 可照抄形态(引号内换行合法):')
3157
3354
  console.error(' sillyspec flow start --change <名> --input "<一句 10-30 字中文概括(首行即变更标题,硬区间 10-30 必须、建议 ~20 字);随后动机与背景')
3158
3355
  console.error('')
package/src/index.js CHANGED
@@ -5395,6 +5395,7 @@ SillySpec config — local.yaml 配置键速查
5395
5395
  sillyspec config known-failures list 列 known_failures 豁免键
5396
5396
  sillyspec config known-failures add <key…> 增豁免键(行级手术保注释、原子写;坏 YAML 拒改)
5397
5397
  sillyspec config known-failures remove <key…> 删豁免键(未命中静默忽略)
5398
+ sillyspec config validate [--json] 逐键校验 local.yaml(错误 exit 1;缺文件=零配置可用 exit 0)
5398
5399
 
5399
5400
  说明:
5400
5401
  - 数据源:src/config-schema.js(唯一真相;reader 见各键「读取点」)
@@ -5436,6 +5437,17 @@ SillySpec config — local.yaml 配置键速查
5436
5437
  if (!content.endsWith('\n')) process.stdout.write('\n')
5437
5438
  break;
5438
5439
  }
5440
+ // config validate(2026-10-23-config-validate-skill):逐键校验(schema 单一源;YAML/
5441
+ // 类型/枚举/段内必填凭据闭合,未知键警告)——agent 写完配置的硬验证门(技能末步=本命令
5442
+ // exit 0 才算完成);缺文件=零配置可用 info exit 0(69 键仅凭据族真必填)。
5443
+ if (configSub === 'validate') {
5444
+ let specBase = null
5445
+ try { specBase = resolvePlatformSpecDir(dir, specDir) } catch { /* 同 cat:pointer 异常走祖先链 */ }
5446
+ const { cmdConfigValidate } = await import('./config-validate.js')
5447
+ const exitCode = cmdConfigValidate({ dir, specBase, json })
5448
+ process.exitCode = exitCode
5449
+ break;
5450
+ }
5439
5451
  // config known-failures(2026-10-21-p0-p1-hardening FR-07 / dsh z2 实证:known_failures
5440
5452
  // 无 CLI 管理面 → agent 手改 local.yaml,PS5.1 编码把 50KB 文件写成 mojibake 等用户
5441
5453
  // 2h13m)。行级增删查:保注释、原子写、坏 YAML 拒改——消灭手改场景。
@@ -5456,10 +5468,10 @@ SillySpec config — local.yaml 配置键速查
5456
5468
  break;
5457
5469
  }
5458
5470
  if (!isSchema) {
5459
- const sug = didYouMean(configSub, ['schema', 'cat', 'known-failures']);
5471
+ const sug = didYouMean(configSub, ['schema', 'cat', 'validate', 'known-failures']);
5460
5472
  console.error(`❌ 未知子命令: config ${configSub}`);
5461
5473
  if (sug) console.error(` 你是想输入「config ${sug}」吗?`);
5462
- console.error('用法: sillyspec config [schema|cat|known-failures] [--json] 打印配置键 / 读内容 / 管理 known_failures 键');
5474
+ console.error('用法: sillyspec config [schema|cat|validate|known-failures] [--json] 打印配置键 / 读内容 / 校验 / 管理 known_failures 键');
5463
5475
  process.exit(2);
5464
5476
  }
5465
5477
  const { renderSchemaHuman, renderSchemaJson } = await import('./config-schema.js');
@@ -436,6 +436,105 @@ export async function runReviewDispatch(opts = {}) {
436
436
  }
437
437
  }
438
438
 
439
+ // ════════ 薄道评审平台单调用派发(2026-10-23-review-inline-dispatch)════════
440
+ /**
441
+ * 薄道(thin flow)review 子步的平台单调用通道:flow done 评审缺席分支经 local.yaml
442
+ * review_inline_wait 键门控调用——薄道任务书(renderReviewerTaskbook 产物+平台产出方式
443
+ * 尾注,任务书生产权仍在 CLI)派发平台 readOnly worker,有界轮询至完成/超时,三通道
444
+ * 提取复用厚道 extractReviewFromArtifacts(JSON 对象提取与 schema 无关)+ 薄道
445
+ * validateReviewJson 同一源校验,通过才落 changeDir/review.json(reviewer=platform-inline
446
+ * 如实标注)——成功后调用方同调用继续登记子步,missing→PASS 往返消灭。
447
+ * 任何失败返回 {ok:false, reason}——调用方回退现状任务书面(零治理损失)。
448
+ * 不写 dispatch record(那是厚道跨命令异步形态;单调用在途态只随返回值与输出留痕)。
449
+ */
450
+ export async function runThinReviewInline(opts = {}) {
451
+ const {
452
+ cwd = process.cwd(), specBase, change, changeDir, taskbookText,
453
+ waitMs = 60_000, pollIntervalMs = 5_000, budgetUsd = 1.0,
454
+ client = null, probe = null, log = null,
455
+ } = opts
456
+ const say = typeof log === 'function' ? log : () => {}
457
+ if (!specBase || !change || !changeDir || typeof taskbookText !== 'string' || !taskbookText.trim()) {
458
+ return { ok: false, reason: '缺少 specBase/change/changeDir/taskbookText' }
459
+ }
460
+ let cl = client
461
+ if (!cl) {
462
+ try {
463
+ const { SillyHubMcpClient } = await import('./sillyhub-mcp/client.js')
464
+ cl = new SillyHubMcpClient({ cwd })
465
+ } catch (e) {
466
+ return { ok: false, reason: `client 构建失败(${(e && e.message) || e})` }
467
+ }
468
+ }
469
+ if (!cl || typeof cl.createMission !== 'function' || typeof cl.dispatchWorker !== 'function'
470
+ || typeof cl.listWorkers !== 'function' || typeof cl.getWorkerResult !== 'function') {
471
+ return { ok: false, reason: 'client 不具备平台派发接口(create/dispatch/list/result)' }
472
+ }
473
+ try {
474
+ const probeFn = probe || (await import('./dispatch/probe.js')).probeSillyHub
475
+ const pr = await probeFn({ client: cl, worktreePath: null, cwd })
476
+ if (!pr || pr.available !== true) {
477
+ return { ok: false, reason: `平台通道不可用(${pr && pr.reason ? pr.reason : 'probe 异常'})` }
478
+ }
479
+ const objective = `轻量变更独立评审(${change})——按任务书产出 review.json 结论`
480
+ const workerPrompt = taskbookText + '\n' + [
481
+ '',
482
+ '【平台通道产出方式(read_only 约束——覆盖上方「写到文件」指引)】',
483
+ '- 平台 worker 禁止写任何文件、禁止 git 操作:把完整 review.json 文本(上方 schema)作为产出返回。',
484
+ '- 优先放 artifacts(kind 含 review,content 字段放纯 JSON 文本);宿主不支持 artifacts 时放回答正文首个 ```json 代码块(CLI 回收链三通道兜底)。',
485
+ ].join('\n')
486
+ const mission = await cl.createMission({ objective, orchestrationMode: 'external', budgetUsd })
487
+ const missionId = mission && mission.missionId
488
+ if (!missionId) return { ok: false, reason: 'create_mission 未返回 missionId' }
489
+ say(`🛰️ 平台评审已派发(mission ${String(missionId).slice(0, 8)})——有界等待 ${Math.round(waitMs / 1000)}s(review_inline_wait)`)
490
+ let worker = null
491
+ try {
492
+ worker = await cl.dispatchWorker({ missionId, objective, readOnly: true, worktreePath: cwd, workerPrompt })
493
+ } catch (e) {
494
+ return { ok: false, reason: `dispatch_worker 异常(${(e && e.message) || e})`, missionId }
495
+ }
496
+ const workerId = worker && worker.workerId
497
+ if (!workerId) return { ok: false, reason: 'dispatch_worker 未返回 workerId', missionId }
498
+ const deadline = Date.now() + Math.max(1000, waitMs)
499
+ let lastState = ''
500
+ while (Date.now() < deadline) {
501
+ await new Promise((r) => setTimeout(r, Math.max(200, Math.min(pollIntervalMs, deadline - Date.now()))))
502
+ let w = null
503
+ try {
504
+ const workers = await cl.listWorkers(missionId)
505
+ w = Array.isArray(workers) ? workers.find((x) => x && (x.id === workerId || x.worker_id === workerId)) : null
506
+ } catch { /* 单次轮询抖动按在途继续(下轮重试) */ }
507
+ const st = w ? String(w.status || '') : 'unknown'
508
+ if (st !== lastState) {
509
+ say(`🛰️ 平台评审状态:${st}(剩 ${Math.max(0, Math.ceil((deadline - Date.now()) / 1000))}s)`)
510
+ lastState = st
511
+ }
512
+ if (st === 'failed' || st === 'killed') return { ok: false, reason: `worker 终态 ${st}`, missionId, workerId }
513
+ if (st === 'completed') {
514
+ const gr = await cl.getWorkerResult({ missionId, workerId })
515
+ const extracted = extractReviewFromArtifacts(gr && gr.artifacts, gr && gr.answer)
516
+ if (!extracted || typeof extracted !== 'object') {
517
+ return { ok: false, reason: 'worker completed 但三通道提取不到 review JSON(artifacts kind/文本、answer 正文)——平台 UI 可回看 mission', missionId, workerId }
518
+ }
519
+ // reviewer 如实标注(覆盖 worker 任意填法);change 对齐本变更;其余字段产物原样不改写
520
+ const candidate = { ...extracted, change, reviewer: 'platform-inline' }
521
+ const reviewPath = join(changeDir, 'review.json')
522
+ writeAtomicSync(reviewPath, JSON.stringify(candidate, null, 2) + '\n')
523
+ const { validateReviewJson } = await import('./flow-review.js')
524
+ const v = validateReviewJson(reviewPath)
525
+ if (!v.ok) {
526
+ try { rmSync(reviewPath, { force: true }) } catch { /* 删件失败由下轮覆盖兜底 */ }
527
+ return { ok: false, reason: `提取的 review 不符薄道 schema(${v.errors.join(';')})——原文可参考平台 mission UI`, missionId, workerId }
528
+ }
529
+ return { ok: true, missionId, workerId, reviewPath }
530
+ }
531
+ }
532
+ return { ok: false, reason: `等待 ${Math.round(waitMs / 1000)}s 未完成(在途 mission ${missionId} 可在平台 UI 回看;本轮回退任务书面)`, missionId, workerId }
533
+ } catch (e) {
534
+ return { ok: false, reason: `inline 派发异常(${(e && e.message) || String(e)})` }
535
+ }
536
+ }
537
+
439
538
  // ════════ 影子期派发(2026-09-18-ceremony-risk-pricing task-06 / FR-04 / D-007@v1 / R-04 / R-06)════════
440
539
  // 依据:design.md「总体方案 Phase 3」+「风险登记 R-04/R-06」+ plan.md「Wave 4」+ 全局硬约束 2。
441
540
  // 定位:S0/S1 轻档变更在明面轻仪主线外,后台静默派发重仪式独立评审**只记账不阻断**——
@@ -883,6 +883,26 @@ export async function bakeArchiveTimeline({ cwd, specBase, changeName, destDir,
883
883
  *
884
884
  * 纯函数(git 读数由调用方喂入),单测直接钉构成口径。
885
885
  * @param {{changeName:string, stagedNameStatus:string, headTreeFiles:string, existsFn?:(p:string)=>boolean}} p
886
+ /**
887
+ * 归档落库判定(2026-10-23-flow-done-shared-repo-multi-session-frictions 缺陷③):HEAD 树中
888
+ * 是否已含本变更的归档目录条目(.sillyspec/changes/archive/<changeName>/ 下一层首段)。
889
+ * 目录名口径:archiveDestDirName 现行=裸变更名;`-<changeName>` 后缀兼容历史日期前缀形态。
890
+ * 纯函数(headTreeFiles 由调用方 git ls-tree 取好传入,测试零 git 依赖)。
891
+ */
892
+ export function archiveLandedInHeadTree({ headTreeFiles, changeName }) {
893
+ const prefix = '.sillyspec/changes/archive/'
894
+ const name = String(changeName || '')
895
+ if (!name) return false
896
+ for (const line of String(headTreeFiles || '').split('\n')) {
897
+ const p = line.trim().replace(/\\/g, '/')
898
+ if (!p.startsWith(prefix)) continue
899
+ const first = p.slice(prefix.length).split('/')[0]
900
+ if (first === name || first.endsWith('-' + name)) return true
901
+ }
902
+ return false
903
+ }
904
+
905
+ /**
886
906
  * @returns {{pathspecs:string[], dropped:string[]}}
887
907
  */
888
908
  export function resolveArchiveCommitPathspecs({ changeName, stagedNameStatus, headTreeFiles, existsFn }) {
@@ -1152,12 +1172,26 @@ try {
1152
1172
  existsFn: (p) => { try { return existsSync(join(cwd, p)) } catch { return false } },
1153
1173
  })
1154
1174
  if (pathspecs.length > 0) {
1155
- console.log(`🧾 归档提交一笔到位(src 锚 HEAD 树/归档侧目录 pathspec/共享面按在场——共享暂存区竞态加固):`)
1156
- console.log(` git commit -m "chore(archive): ${changeName} 归档留档" -- ${pathspecs.join(' ')}`)
1175
+ // 落库断言(2026-10-23-flow-done-shared-repo-multi-session-frictions 缺陷③):打印命令≠落库
1176
+ // ——共享暂存区竞态下命令没人执行/被并行会话抢跑,都呈现「归档目录全 staged(A/R)+ 源侧
1177
+ // 删除停 D + flow done 报成功」的半态(实测 pull/清理丢归档)。终态不得只以打印当成功:
1178
+ // HEAD 树含本变更归档路径=已落库(ℹ️ 语义不变);未落库=🚨 升级 stderr + 可复制命令 + 后果。
1179
+ const landed = archiveLandedInHeadTree({ headTreeFiles: String(headTreeRaw || ''), changeName })
1180
+ const out = landed ? console.log : console.error
1181
+ if (!landed) {
1182
+ console.error(`🚨 归档提交未落库:归档目录已进暂存区(A/R)但 HEAD 树中无 .sillyspec/changes/archive/ 下本变更路径——只打印不执行 ≠ 归档成功(共享仓竞态:命令未被本会话执行或被并行会话抢跑)`)
1183
+ console.error(` 立即执行(幂等;pathspec 限本变更面):`)
1184
+ } else {
1185
+ console.log(`🧾 归档提交一笔到位(归档路径已在 HEAD 树——余下 staged 面一笔带走;src 锚 HEAD 树/归档侧目录 pathspec/共享面按在场——共享暂存区竞态加固):`)
1186
+ }
1187
+ out(` git commit -m "chore(archive): ${changeName} 归档留档" -- ${pathspecs.join(' ')}`)
1157
1188
  if (dropped.length > 0) {
1158
- console.log(` ⚠️ 丢弃 ${dropped.length} 个瞬时暂存条目(HEAD 不在册且工作区不在场——并行会话动了共享暂存区,未入命令):${dropped.join('、')}`)
1189
+ out(` ⚠️ 丢弃 ${dropped.length} 个瞬时暂存条目(HEAD 不在册且工作区不在场——并行会话动了共享暂存区,未入命令):${dropped.join('、')}`)
1190
+ }
1191
+ out(` (若执行报 pathspec 不匹配:共享仓暂存区在打印→执行间又被并行会话改动——git diff --cached --name-only 全量重核,以实际面替换 pathspec 后再提交)`)
1192
+ if (!landed) {
1193
+ console.error(` 不执行的后果:归档件只存在于暂存区——下一次 pull/清理/工作区重置会丢归档。执行后核对:git log --oneline -1 应见「chore(archive): ${changeName} 归档留档」。`)
1159
1194
  }
1160
- console.log(` (若执行报 pathspec 不匹配:共享仓暂存区在打印→执行间又被并行会话改动——git diff --cached --name-only 全量重核,以实际面替换 pathspec 后再提交)`)
1161
1195
  }
1162
1196
  } catch { /* 提示失败不阻断归档(advisory) */ }
1163
1197
  if (othersResidual.length > 0) {
@@ -358,8 +358,10 @@ export async function resolveQuickLinkedChanges({ pm, cwd, specDir, quickFiles,
358
358
  // 无 /g 标志无状态,提级零行为差):
359
359
  // ① 路径段匹配:任意层出现 src / test / tests / __tests__ 段(src-guide 等长名不误蹭——按段全等)
360
360
  // ② 代码扩展名兜底:src/test 目录约定外的代码(backend/app/**.py、scripts/*.go 等)
361
- const CODE_PATH_SEGMENT_RE = /(^|\/)(src|tests?|__tests__)(\/|$)/
362
- const CODE_EXTENSION_RE = /\.(?:js|mjs|cjs|ts|tsx|jsx|py|pyw|go|rs|java|kt|kts|rb|php|cs|c|h|cpp|cc|hpp|swift|scala|groovy|vue|svelte)$/
361
+ // export(2026-10-23-turbo-docs-auto):flow.js docs-auto 档纯文档面判定同源消费——
362
+ // 分类器单一源(宁可多跑不可漏跑口径),本体零变化
363
+ export const CODE_PATH_SEGMENT_RE = /(^|\/)(src|tests?|__tests__)(\/|$)/
364
+ export const CODE_EXTENSION_RE = /\.(?:js|mjs|cjs|ts|tsx|jsx|py|pyw|go|rs|java|kt|kts|rb|php|cs|c|h|cpp|cc|hpp|swift|scala|groovy|vue|svelte)$/
363
365
 
364
366
  // 测试文件判定:目录段(test/tests/__tests__——比 quick-gate-profile 的 TEST_DIR_RE 多认裸
365
367
  // test/ 段,本仓 test/ 下即有无 .test. 后缀的 .mjs)+ basename test_ 前缀 + .test./_test./.spec.
@@ -2162,6 +2162,7 @@ export function runVerifyTestCheck({ cwd, specBase, changeName = null, ctx = nul
2162
2162
  // ① 本变更测试 ∪ ③ import 依赖(discoverModuleDependentTests);② FR 关联回归(fail-open)。
2163
2163
  let depsAutoFiles = []
2164
2164
  let frPre = null
2165
+ let frCapApplied = null
2165
2166
  if (strategy === 'dynamic' && !gitUnavailable) {
2166
2167
  try {
2167
2168
  depsAutoFiles = discoverModuleDependentTests({ cwd, changedFiles: lastChangedFiles, coveredCommands: [] })
@@ -2169,6 +2170,17 @@ export function runVerifyTestCheck({ cwd, specBase, changeName = null, ctx = nul
2169
2170
  try {
2170
2171
  frPre = collectFrLinkedTests({ specBase, changeName, changedFiles: lastChangedFiles, projectRoot: cwd })
2171
2172
  } catch { frPre = null }
2173
+ // 帽在 frPre 产生点一次成型(2026-10-23-fr-regress-default-off):决策空判/展示计数/执行
2174
+ // 组面全用帽后面——单一时点单一口径(v2 缺省 0=off:FR 层不进面即不触发 dynamic-subset
2175
+ // 空跑);capApplied 随行传执行层出披露行(total=真实绑定面,防二次施帽口径失真)。
2176
+ try {
2177
+ const capVal = readFrRegressCap(specBase)
2178
+ if (frPre && capVal !== null) {
2179
+ const capped = applyFrRegressCap(frPre, capVal)
2180
+ frPre = capped.fr
2181
+ frCapApplied = capped.capApplied
2182
+ }
2183
+ } catch { /* 键读取 fail-soft=缺省 off */ }
2172
2184
  }
2173
2185
  const scopeFiles = frPre ? [...new Set([...depsAutoFiles, ...frPre.files])] : [...new Set(depsAutoFiles)]
2174
2186
  const action = decideVerifyTestAction({ strategy, scopeCount: scopeFiles.length, gitUnavailable })
@@ -2235,11 +2247,13 @@ export function runVerifyTestCheck({ cwd, specBase, changeName = null, ctx = nul
2235
2247
  incremental = computeIncrementalFace({ ledger, filesSince, fullFace: lastChangedFiles })
2236
2248
  }
2237
2249
  }
2250
+ // frCount 展示口径随帽(2026-10-23-fr-regress-default-off):frPre 已在产生点施帽——
2251
+ // 展示计数/空判/执行同源(见上方帽一次成型注释)。
2238
2252
  const frCount = frPre && frPre.files.length ? frPre.files.length : 0
2239
2253
  if (incremental) {
2240
2254
  subsetInput = incremental.files
2241
2255
  console.log(`⚡ ${incremental.note}`)
2242
- console.log(`ℹ️ 动态测试子集(增量重跑档):修复面三源重算(本变更测试 ∪ FR 关联回归 ∪ import 依赖),runner 自项目结构推断。恒全子集:local.yaml verify: test_rerun: full`)
2256
+ console.log(`ℹ️ 动态测试子集(增量重跑档):修复面三源重算(本变更测试 ∪ import 依赖;FR 关联回归层按 fr_regress_cap——缺省不跑归 pre-push/CI 门,2026-10-23-fr-regress-default-off),runner 自项目结构推断。恒全子集:local.yaml verify: test_rerun: full`)
2243
2257
  mainResult = runModuleSubset({ cwd, specBase, changeName, hits: [], knownFailures, changedFiles: subsetInput, frPre: null })
2244
2258
  if (mainResult && typeof mainResult === 'object') {
2245
2259
  mainResult.mode = 'incremental-rerun'
@@ -2248,8 +2262,8 @@ export function runVerifyTestCheck({ cwd, specBase, changeName = null, ctx = nul
2248
2262
  : `增量重跑通过:${incremental.note}`
2249
2263
  }
2250
2264
  } else {
2251
- console.log(`ℹ️ 动态测试子集(缺省):本变更测试 ∪ FR 关联回归 ∪ import 依赖 = ${scopeFiles.length} 个(并集去重后的总数;分量 deps ${depsAutoFiles.length}${frCount ? ` + FR 绑定 ${frCount}` : ''},重叠只计一次);runner 自项目结构推断。显式 test_strategy: full 恢复全量`)
2252
- mainResult = runModuleSubset({ cwd, specBase, changeName, hits: [], knownFailures, changedFiles: lastChangedFiles, frPre, observability })
2265
+ console.log(`ℹ️ 动态测试子集(缺省):本变更测试 ∪ import 依赖${frCount ? ' ∪ FR 关联回归' : '(FR 关联回归层缺省不跑归 pre-push/CI 门——fr_regress_cap: N|all 可开启)'} = ${scopeFiles.length} 个(并集去重后的总数;分量 deps ${depsAutoFiles.length}${frCount ? ` + FR 绑定 ${frCount}` : ''},重叠只计一次);runner 自项目结构推断。显式 test_strategy: full 恢复全量`)
2266
+ mainResult = runModuleSubset({ cwd, specBase, changeName, hits: [], knownFailures, changedFiles: lastChangedFiles, frPre, frCapApplied, observability })
2253
2267
  }
2254
2268
  // ledger 记账(无论增/全):head=当前 HEAD(git 失败 null → 下轮自动回全子集);失败批文件
2255
2269
  // 落账供下轮增量;passed 后 failedFiles 清空 → 下轮自然回全子集基线
@@ -2266,15 +2280,23 @@ export function runVerifyTestCheck({ cwd, specBase, changeName = null, ctx = nul
2266
2280
  } catch { /* 记账失败不阻断(下轮退化全子集) */ }
2267
2281
  } else {
2268
2282
  // —— dynamic-empty:变更与测试面零关系——
2269
- // 无自身测试、无 import 依赖测试、无 FR 关联绑定 → 不硬跑全量(防超时/预存失败面,
2270
- // 同旧 module 0 命中防误伤初衷);全量覆盖请显式 test_strategy: full。
2283
+ // 无自身测试、无 import 依赖测试(+FR 关联回归层:v2 缺省跳过或绑定面为空)→ 不硬跑
2284
+ // 全量(防超时/预存失败面,同旧 module 0 命中防误伤初衷);全量覆盖请显式 test_strategy: full。
2285
+ // FR 层被帽跳过(frCapApplied 在场 total>0)≠「无绑定」——reason 如实陈述并打披露行
2286
+ // (2026-10-23-fr-regress-default-off 评审 P1 修复:缺省跳过恒披露不静默)。
2287
+ if (frCapApplied && frCapApplied.total > 0) {
2288
+ console.log(`ℹ️ fr_regress_cap:FR 关联回归面 ${frCapApplied.kept}/${frCapApplied.total} 进实测面,其余 ${frCapApplied.total - frCapApplied.kept} 个归 pre-push/CI 门(可开启:local.yaml fr_regress_cap: <N|all>——N=前 N 个 / all=全跑旧行为;缺省不跑)`)
2289
+ }
2290
+ const frEmptyNote = frCapApplied && frCapApplied.total > 0
2291
+ ? `FR 关联回归层被 fr_regress_cap 跳过(绑定 ${frCapApplied.total} 个未跑,归 pre-push/CI 门)`
2292
+ : '无 FR 关联绑定'
2271
2293
  mainResult = {
2272
2294
  status: 'skipped',
2273
2295
  command: null,
2274
2296
  exitCode: null,
2275
2297
  durationMs: null,
2276
2298
  outputTail: null,
2277
- reason: `变更与测试面零关系(${lastChangedFiles.length} 个变更文件无自身测试、无 import 依赖测试、无 FR 关联绑定)——动态子集空,不硬跑全量(防超时/预存失败面)。全量覆盖请显式 test_strategy: full。` + eaNote,
2299
+ reason: `变更与测试面零关系(${lastChangedFiles.length} 个变更文件无自身测试、无 import 依赖测试、${frEmptyNote})——动态子集空,不硬跑全量(防超时/预存失败面)。全量覆盖请显式 test_strategy: full。` + eaNote,
2278
2300
  resultPath: null,
2279
2301
  mode: 'dynamic-empty',
2280
2302
  fallbackReason: null,
@@ -3349,7 +3371,44 @@ export function applyTraceResidual({ mainResult, action, cwd, specBase, changeNa
3349
3371
  * export 供直测(buildDepsBatches 同款先例——fixture 需真跑测试文件,不宜经
3350
3372
  * runVerifyTestCheck 全链只为断言组卷接线)。
3351
3373
  */
3352
- export function runModuleSubset({ cwd, specBase, changeName, hits, knownFailures = [], changedFiles = [], frPre = null, observability = null }) {
3374
+ /**
3375
+ * fr_regress_cap 读取 v2(2026-10-23-fr-regress-default-off,缺省翻转:用户裁决「默认就不跑,
3376
+ * 配置可以开启」):缺省(无键)/坏值/不可读 → 0(off——FR 关联回归层本地不跑,归 pre-push/CI
3377
+ * 门);off → 0;N(1-2000 正整数)→ 前 N 个;all → null(无帽=旧行为,显式恢复全跑)。
3378
+ * normalizeLineEndings CRLF 归一保留;轻量正则不引 yaml 依赖(extractTestCommand 同风格)。
3379
+ */
3380
+ export function readFrRegressCap(specBase) {
3381
+ try {
3382
+ const raw = normalizeLineEndings(readFileSync(join(specBase, 'local.yaml'), 'utf8'))
3383
+ const m = raw.match(/^[ \t]*fr_regress_cap:[ \t]*([^\n#]+?)[ \t]*(?:#.*)?$/m)
3384
+ if (!m) return 0 // 缺省=off(v2 翻转)
3385
+ const v = m[1].trim().replace(/^["']|["']$/g, '')
3386
+ if (/^all$/i.test(v)) return null // 显式恢复旧行为(无帽整跑)
3387
+ if (/^off$/i.test(v)) return 0
3388
+ if (/^\d+$/.test(v)) {
3389
+ const n = Number(v)
3390
+ if (n >= 1 && n <= 2000) return n
3391
+ }
3392
+ return 0 // 坏值 fail-soft=新缺省 off
3393
+ } catch { return 0 } // 不可读 fail-soft=新缺省 off
3394
+ }
3395
+
3396
+ /**
3397
+ * FR 回归面帽纯函数(2026-10-23-fr-regress-cap):fr.files 超帽截前 N 个(现有稳定序);
3398
+ * cap=0(off)整层归零;null/undefined/帽内零截断 → 原对象零变化。返回 { fr, capApplied },
3399
+ * capApplied={kept,total}|null 供 loud 披露。下游(frLinked/frReport/优先面标记/批组卷)
3400
+ * 从帽后面对象自然推导,无第二套口径。
3401
+ */
3402
+ export function applyFrRegressCap(fr, cap) {
3403
+ if (cap === null || cap === undefined || !fr || !Array.isArray(fr.files) || fr.files.length === 0) {
3404
+ return { fr, capApplied: null }
3405
+ }
3406
+ if (cap === 0) return { fr: { ...fr, files: [] }, capApplied: { kept: 0, total: fr.files.length } }
3407
+ if (fr.files.length <= cap) return { fr, capApplied: null }
3408
+ return { fr: { ...fr, files: fr.files.slice(0, cap) }, capApplied: { kept: cap, total: fr.files.length } }
3409
+ }
3410
+
3411
+ export function runModuleSubset({ cwd, specBase, changeName, hits, knownFailures = [], changedFiles = [], frPre = null, frCapApplied = null, observability = null }) {
3353
3412
  const subsetStartedAt = Date.now()
3354
3413
  const perModule = hits.map(h => runOneModule(h.name, h.test, cwd, knownFailures))
3355
3414
  // 依赖测试伪模块(module-test-face-rot,2026-09-16 本会话实证):硬编码模块测试清单不含
@@ -3365,6 +3424,17 @@ export function runModuleSubset({ cwd, specBase, changeName, hits, knownFailures
3365
3424
  let fr = frPre
3366
3425
  if (!fr) {
3367
3426
  try { fr = collectFrLinkedTests({ specBase, changeName, changedFiles: changedFiles || [], projectRoot: cwd }) } catch { fr = null }
3427
+ // 本地收集路径同样施帽(2026-10-23-fr-regress-default-off 单一口径:直调方/增量重跑未带
3428
+ // frPre——v2 缺省 0=off,FR 层本地不跑归 pre-push/CI 门;all=null 无帽旧行为)。frPre 由
3429
+ // 调用方在产生点施帽并随行带 frCapApplied(防二次施帽口径失真),本块不再重复施帽。
3430
+ try {
3431
+ const capVal = readFrRegressCap(specBase)
3432
+ if (fr && capVal !== null) {
3433
+ const capped = applyFrRegressCap(fr, capVal)
3434
+ fr = capped.fr
3435
+ frCapApplied = capped.capApplied
3436
+ }
3437
+ } catch { /* 键读取 fail-soft=缺省 off */ }
3368
3438
  }
3369
3439
  // 优先面全量口径(2026-10-06-fr-priority-overlap):frLinked 只含与 deps 无重叠的「新增」
3370
3440
  // 绑定文件——重叠者此前拿不到优先权,仍按普通依赖受帽字母序弃置(resume-title 收口实测
@@ -3382,6 +3452,9 @@ export function runModuleSubset({ cwd, specBase, changeName, hits, knownFailures
3382
3452
  console.warn(`⚠️ ${fr.unresolved.length} 个 FR 绑定测试路径无法自仓根解析(不入实测面):${fr.unresolved.slice(0, 3).join('、')}${fr.unresolved.length > 3 ? ' 等' : ''}——sillyspec tests repair-paths 可修`)
3383
3453
  }
3384
3454
  }
3455
+ if (frCapApplied) {
3456
+ console.log(`ℹ️ fr_regress_cap:FR 关联回归面 ${frCapApplied.kept}/${frCapApplied.total} 进实测面,其余 ${frCapApplied.total - frCapApplied.kept} 个归 pre-push/CI 门(可开启:local.yaml fr_regress_cap: <N|all>——N=前 N 个 / all=全跑旧行为;缺省不跑)`)
3457
+ }
3385
3458
  const depsAll = [...new Set([...deps, ...frLinked])].sort()
3386
3459
  let depsBatches = []
3387
3460
  if (depsAll.length > 0) {