sillyspec 3.28.0 → 3.28.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/.claude/skills/sillyspec-auto/SKILL.md +54 -93
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -114
  3. package/.claude/skills/sillyspec-continue/SKILL.md +17 -47
  4. package/.claude/skills/sillyspec-execute/SKILL.md +222 -214
  5. package/.claude/skills/sillyspec-knowledge/SKILL.md +40 -270
  6. package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
  7. package/.claude/skills/sillyspec-propose/SKILL.md +16 -87
  8. package/.claude/skills/sillyspec-resume/SKILL.md +21 -70
  9. package/.claude/skills/sillyspec-state/SKILL.md +25 -64
  10. package/.claude/skills/sillyspec-workspace/SKILL.md +33 -175
  11. package/package.json +1 -1
  12. package/src/archive-delta.js +462 -0
  13. package/src/constants.js +6 -0
  14. package/src/design-facts.js +418 -0
  15. package/src/dispatch/backends/sillyhub-mcp.js +17 -5
  16. package/src/docs-check.js +197 -34
  17. package/src/docs-migrate.js +124 -0
  18. package/src/doctor-diagnostics.js +11 -2
  19. package/src/endpoint-baseline.js +109 -0
  20. package/src/git-helper.js +31 -0
  21. package/src/index.js +381 -22
  22. package/src/modules.js +41 -12
  23. package/src/progress/change-registry.js +39 -0
  24. package/src/progress/consistency-doctor.js +124 -5
  25. package/src/progress/stage-machine.js +13 -2
  26. package/src/progress.js +5 -0
  27. package/src/quicklog.js +51 -0
  28. package/src/run/command.js +199 -5
  29. package/src/run/complete-handlers.js +60 -4
  30. package/src/run/complete.js +105 -6
  31. package/src/run/gates.js +353 -2
  32. package/src/run/progress-confirm.js +66 -0
  33. package/src/run/prompt.js +193 -15
  34. package/src/run/quick-audit.js +12 -5
  35. package/src/run/scan-profile.js +117 -7
  36. package/src/run/shared.js +22 -6
  37. package/src/run/stage.js +14 -1
  38. package/src/scan-diff.js +54 -1
  39. package/src/scan-facts.js +173 -0
  40. package/src/scan-postcheck.js +114 -3
  41. package/src/spec-sync.js +19 -2
  42. package/src/stage-review.js +62 -3
  43. package/src/stages/archive.js +5 -5
  44. package/src/stages/brainstorm-auto.js +2 -1
  45. package/src/stages/brainstorm.js +12 -20
  46. package/src/stages/execute.js +19 -21
  47. package/src/stages/plan-postcheck.js +194 -1
  48. package/src/stages/plan.js +8 -3
  49. package/src/stages/quick.js +4 -2
  50. package/src/stages/scan.js +44 -102
  51. package/src/stages/verify.js +18 -18
  52. package/src/task-review.js +200 -0
  53. package/src/taskcard.js +8 -1
  54. package/src/verify-postcheck.js +771 -29
  55. package/src/verify-probes.js +89 -10
  56. package/src/workflow.js +30 -0
  57. package/templates/agents-instruction.md +1 -1
  58. package/templates/prompts/taskcard-rules.md +5 -0
  59. package/templates/workflows/scan-docs.yaml +6 -0
  60. package/.claude/skills/verify-per-user/SKILL.md +0 -6
@@ -0,0 +1,418 @@
1
+ /**
2
+ * design-facts — 设计事实核验 + design 骨架渲染纯函数集
3
+ * (change: 2026-09-07-ir-stage-p3c,task-01,FR-01/FR-02,D-001@v1/D-002@v1/D-004@v1)
4
+ *
5
+ * 职责(设计文档 Wave 1 + Wave 2 的纯函数本体,接线在 task-02/03):
6
+ * 1. parseDecisionDomains(decisionsText):decisions.md 文本 → 当前版本 D 条目的模块域投影
7
+ * [{ id, domains }](NEW: 前缀项原样保留,豁免存在性核验)。
8
+ * 2. loadModuleMap(specRoot, project):_module-map.yaml → { ids, prefixPairs } | null。
9
+ * 3. validateDecisionModuleRefs({ changeDir, specRoot, project }):模块域分级核验
10
+ * (ERROR=幻觉模块/书写错误,WARNING=声明域×实改面差异,skipped=无索引/无决策)。
11
+ * 4. generateDesignSkeleton({ changeName, decisionsText, author, now }):design.md 十三章节
12
+ * 骨架字符串(章节标题逐字取自 brainstorm Step 6 模板,R-03——禁止自创措辞)。
13
+ *
14
+ * 依赖方向(R-01 单向防环):design-facts → decision-distill / change-list / modules,
15
+ * 三者均不反向 import 本模块。条目解析双源说明:distill 的 parseDecisions 只吃 changeDir
16
+ * (内部读盘),故文本入口 parseDecisionDomains 持有其行循环的逐字镜像(正则/字段映射/
17
+ * 列表切分全部同源复制),validateDecisionModuleRefs 则直接复用 parseDecisions 导出——
18
+ * 双解析一致性由 task-04 的同一 fixture 双源测试锁定。
19
+ *
20
+ * 信封 code(task-02 接线层使用,本模块已内嵌在消息前缀):
21
+ * decision_module_ref_invalid(ERROR)/ decision_module_domain_gap(WARNING)/
22
+ * decision_module_check_skipped(skipped 原因字符串,接线层包 code)。
23
+ */
24
+ import { existsSync, readFileSync } from 'node:fs'
25
+ import { join } from 'node:path'
26
+ import { parseDecisions } from './decision-distill.js'
27
+ import { parseFileChangeListDetailed } from './change-list.js'
28
+ import { parseModuleMapSimple } from './modules.js'
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // 条目解析(distill parseDecisions 的同源镜像,R-01)
32
+ // ---------------------------------------------------------------------------
33
+
34
+ /** 模块域值解析(decision-distill.js:47 parseListValue 逐字镜像):`[a, b]` / `a、b` / `a b` → ['a','b'] */
35
+ function parseListValue(v) {
36
+ return v.replace(/^\[/, '').replace(/\]$/, '')
37
+ .split(/[,,、\s]+/)
38
+ .map(s => s.trim())
39
+ .filter(Boolean)
40
+ }
41
+
42
+ /** 模块域字段标签集(decision-distill.js:77 applyField 的 domains 分支同款,中英标签+中英冒号兼容) */
43
+ const DOMAIN_LABELS = new Set(['模块域', '模块', 'domains', 'domain'])
44
+
45
+ /**
46
+ * decisions.md 文本 → 裸条目数组(decision-distill.js:98 parseDecisions 行循环的模块域子集镜像:
47
+ * 头部正则 :114 / 字段行正则 :123 逐字同源;仅解析本模块消费的 id/number/version/domains)。
48
+ * @param {string} content decisions.md 全文
49
+ * @returns {Array<{ id: string, number: string, version: number, domains: string[] }>}
50
+ */
51
+ function extractDomainEntries(content) {
52
+ const entries = []
53
+ let cur = null
54
+ const flush = () => {
55
+ if (!cur) return
56
+ entries.push(cur)
57
+ cur = null
58
+ }
59
+ for (const line of String(content ?? '').replace(/\r\n/g, '\n').split('\n')) {
60
+ const h = line.match(/^##\s+(D-\d+)(?:@v(\d+))?\s*(.*)$/)
61
+ if (h) {
62
+ flush()
63
+ const version = h[2] ? parseInt(h[2], 10) : 1
64
+ cur = { id: `${h[1]}@v${version}`, number: h[1], version, domains: [] }
65
+ continue
66
+ }
67
+ if (!cur) continue
68
+ const f = line.match(/^-\s+([^\s::]+)\s*[::]\s*(.*)$/)
69
+ if (f && DOMAIN_LABELS.has(f[1].toLowerCase())) cur.domains = parseListValue(f[2].trim())
70
+ }
71
+ flush()
72
+ return entries
73
+ }
74
+
75
+ /**
76
+ * 当前版本过滤(decision-distill.js:462 highestByNumber 同款):同号只留最高版本
77
+ * (D-xxx@v1 与 @v2 并存时 v1 视为 superseded 旧版剔除)。
78
+ * @template T
79
+ * @param {Array<T & { number: string, version: number }>} entries
80
+ * @returns {T[]}
81
+ */
82
+ function currentVersionEntries(entries) {
83
+ const highestByNumber = new Map()
84
+ for (const e of entries) {
85
+ const prev = highestByNumber.get(e.number)
86
+ if (!prev || e.version > prev.version) highestByNumber.set(e.number, e)
87
+ }
88
+ return [...highestByNumber.values()]
89
+ }
90
+
91
+ /**
92
+ * 纯函数:解析 decisions.md 文本的当前版本决策条目模块域(FR-01)。
93
+ * NEW: 前缀项原样保留在 domains 项内(`NEW:foo` 豁免存在性核验;`NEW: foo` 带空格会被
94
+ * parseListValue 切成裸项 'NEW:'/'foo',由 validateDecisionModuleRefs 报书写错误)。
95
+ * @param {string} decisionsText decisions.md 全文(纯文本入参,不读盘)
96
+ * @returns {Array<{ id: string, domains: string[] }>} 当前版本条目(同号取最高 @vN,首现顺序)
97
+ */
98
+ export function parseDecisionDomains(decisionsText) {
99
+ return currentVersionEntries(extractDomainEntries(decisionsText))
100
+ .map(e => ({ id: e.id, domains: [...e.domains] }))
101
+ }
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // 模块索引(_module-map.yaml 只读,复用 modules.js canonical 解析避免三写)
105
+ // ---------------------------------------------------------------------------
106
+
107
+ /**
108
+ * 读 docs/<project>/modules/_module-map.yaml → 模块 id 集合 + paths 前缀对(FR-01)。
109
+ * 解析复用 modules.js parseModuleMapSimple(canonical 单一真相源);prefixPairs 含
110
+ * paths+core_files(与 decision-distill.js:203 matchImpactsToModules 的取并集口径一致),
111
+ * 目录条目去尾斜杠后按「全等或目录前缀」匹配。
112
+ * @param {string} specRoot 规范根目录(如 <repo>/.sillyspec——与 run/prompt.js 的 specBase 同义)
113
+ * @param {string} project 子项目名(docs/ 下第一段目录名)
114
+ * @returns {{ ids: Set<string>, prefixPairs: Array<{ id: string, path: string }> } | null}
115
+ * 无 map / 不可读 / 解析不出任何模块 → null(无索引不误拦,调用方走 skipped)
116
+ */
117
+ export function loadModuleMap(specRoot, project) {
118
+ if (!specRoot || !project) return null
119
+ const mapPath = join(specRoot, 'docs', project, 'modules', '_module-map.yaml')
120
+ if (!existsSync(mapPath)) return null
121
+ let content
122
+ try { content = readFileSync(mapPath, 'utf8') } catch { return null }
123
+ let parsed = {}
124
+ try { parsed = parseModuleMapSimple(content) || {} } catch { return null }
125
+ const ids = new Set(Object.keys(parsed))
126
+ if (ids.size === 0) return null // 空/失配 map 视同无索引(fail-open,D-002)
127
+ const prefixPairs = []
128
+ for (const [id, m] of Object.entries(parsed)) {
129
+ for (const p of [...((m && m.paths) || []), ...((m && m.core_files) || [])]) {
130
+ const ps = String(p).replace(/\\/g, '/').replace(/\/+$/, '')
131
+ if (ps) prefixPairs.push({ id, path: ps })
132
+ }
133
+ }
134
+ return { ids, prefixPairs }
135
+ }
136
+
137
+ // ---------------------------------------------------------------------------
138
+ // 核验(FR-02 分级:ERROR / WARNING / skipped)
139
+ // ---------------------------------------------------------------------------
140
+
141
+ /**
142
+ * design 文件清单路径 × module-map paths 前缀 → 实改模块集(decision-distill.js:212 同款
143
+ * 匹配语义:全等或目录前缀命中)。未命中任何模块的文件不产生推导(R-02:模块 paths 缺口
144
+ * 不误报,只对命中的做差异)。
145
+ * @param {string[]} filePaths design.md 文件变更清单路径(仓根相对、正斜杠)
146
+ * @param {Array<{ id: string, path: string }>} prefixPairs loadModuleMap 的前缀对
147
+ * @returns {Set<string>} 实改模块 id 集
148
+ */
149
+ export function deriveActualModules(filePaths, prefixPairs) {
150
+ const actual = new Set()
151
+ for (const f of filePaths) {
152
+ const t = String(f || '').replace(/\\/g, '/').replace(/\/+$/, '')
153
+ if (!t) continue
154
+ for (const { id, path: p } of prefixPairs) {
155
+ if (t === p || t.startsWith(p + '/')) { actual.add(id); break }
156
+ }
157
+ }
158
+ return actual
159
+ }
160
+
161
+ /**
162
+ * 核验变更决策的模块域引用(FR-02/D-002/D-004;brainstorm「生成规范文件」步 --done gate 本体,
163
+ * 接线在 task-02)。分级:
164
+ * - ERROR(decision_module_ref_invalid):
165
+ * · 模块域项 ∉ _module-map.yaml 且无 NEW: 前缀(模块幻觉)——出路提示二选一;
166
+ * · 裸项 'NEW:'(`NEW: <名>` 冒号后带空格被列表切分)——书写错误,附正确写法。
167
+ * - WARNING(decision_module_domain_gap):
168
+ * · design.md 文件清单推导的实改模块集 × 声明域并集差异(双向,措辞留人工裁量);
169
+ * · 全部条目均未填模块域 → 汇总一条(存量兼容)。
170
+ * - skipped:decisions.md 不存在 / module-map 缺失(无索引不误拦)→ ok=true + skipped 原因。
171
+ * @param {{ changeDir: string, specRoot: string, project: string }} opts
172
+ * changeDir=变更目录(含 decisions.md 与 design.md);specRoot=规范根;project=子项目名
173
+ * @returns {{ ok: boolean, errors: string[], warnings: string[], skipped?: string }}
174
+ * ok = errors.length === 0(WARNING 不拦);errors/warnings 每条已内嵌信封 code 前缀,
175
+ * skipped 为原因字符串(接线层包 decision_module_check_skipped)。
176
+ */
177
+ export function validateDecisionModuleRefs({ changeDir, specRoot, project } = {}) {
178
+ const errors = []
179
+ const warnings = []
180
+
181
+ if (!changeDir) {
182
+ return { ok: true, errors, warnings, skipped: 'changeDir 未提供——核验跳过' }
183
+ }
184
+ const parsed = parseDecisions(changeDir) // distill 权威条目解析(:98 导出,单向依赖)
185
+ if (parsed.missing) {
186
+ return { ok: true, errors, warnings, skipped: `decisions.md 不存在(${join(changeDir, 'decisions.md')})——无决策条目可核验,跳过` }
187
+ }
188
+ const moduleMap = loadModuleMap(specRoot, project)
189
+ if (!moduleMap) {
190
+ return { ok: true, errors, warnings, skipped: `_module-map.yaml 不存在或不可解析(${join(specRoot || '?', 'docs', project || '?', 'modules', '_module-map.yaml')})——无模块索引不误拦,跳过` }
191
+ }
192
+
193
+ // ① 存在性核验(ERROR):NEW: 前缀豁免;∈ ids 通过;否则幻觉 ERROR(附出路)
194
+ const entries = currentVersionEntries(parsed.entries)
195
+ .map(e => ({ id: e.id, domains: e.domains ? [...e.domains] : [] }))
196
+ let anyDeclared = false
197
+ for (const { id, domains } of entries) {
198
+ if (domains.length > 0) anyDeclared = true
199
+ for (const item of domains) {
200
+ if (item === 'NEW:') {
201
+ errors.push(`[decision_module_ref_invalid] ${id} 模块域书写错误:出现裸项 "NEW:"("NEW: <名>" 冒号后带空格会被列表切分成 "NEW:" 与 "<名>" 两项)——正确写法 NEW:<名>(冒号后不加空格,如 NEW:design-facts)`)
202
+ } else if (item.startsWith('NEW:')) {
203
+ // 新模块声明:豁免存在性核验(D-002/D-004),原样保留
204
+ } else if (!moduleMap.ids.has(item)) {
205
+ errors.push(`[decision_module_ref_invalid] ${id} 模块域含未注册模块 "${item}"——补录 _module-map.yaml 或改用 NEW:${item} 前缀声明新模块`)
206
+ }
207
+ }
208
+ }
209
+
210
+ // ② 全部条目无模块域 → 汇总一条 WARNING(存量兼容,零红门禁)
211
+ if (entries.length > 0 && !anyDeclared) {
212
+ warnings.push(`[decision_module_domain_gap] decisions.md 解析出 ${entries.length} 条当前版本决策,均未填写「模块域」可选字段——模块引用核验无对象(存量兼容放行;建议补填:模块 ID 取自 docs/${project}/modules/_module-map.yaml,新模块用 NEW:<名> 前缀声明)`)
213
+ }
214
+
215
+ // ③ 实改面 × 声明域差异(WARNING 双向;design.md 缺失/无清单时静默跳过——R-02 不产噪)。
216
+ // 声明侧只计已注册的非 NEW: 域:NEW: 域按定义不在 map 内、未注册域已由 ① 报 ERROR,
217
+ // 纳入只会产生重复噪音。
218
+ if (anyDeclared) {
219
+ const fileList = parseFileChangeListDetailed(join(changeDir, 'design.md')).map(e => e.path)
220
+ if (fileList.length > 0) {
221
+ const actual = deriveActualModules(fileList, moduleMap.prefixPairs)
222
+ const declared = new Set()
223
+ for (const { domains } of entries) {
224
+ for (const d of domains) {
225
+ if (!d.startsWith('NEW:') && moduleMap.ids.has(d)) declared.add(d)
226
+ }
227
+ }
228
+ for (const m of actual) {
229
+ if (!declared.has(m)) warnings.push(`[decision_module_domain_gap] 实改模块 "${m}"(design.md 文件清单命中该模块路径)未出现在任何决策的模块域声明中——请确认是遗漏声明(补填模块域)还是文件清单多列`)
230
+ }
231
+ for (const d of declared) {
232
+ if (!actual.has(d)) warnings.push(`[decision_module_domain_gap] 声明模块域 "${d}" 未命中 design.md 文件清单中的任何文件——可能为纯设计影响/跨模块约束(人工裁量即可),也可能是文件清单遗漏`)
233
+ }
234
+ }
235
+ }
236
+
237
+ return { ok: errors.length === 0, errors, warnings }
238
+ }
239
+
240
+ // ---------------------------------------------------------------------------
241
+ // design.md 骨架渲染(Wave 2;R-03:章节标题逐字取自 brainstorm Step 6 模板,禁止自创)
242
+ // ---------------------------------------------------------------------------
243
+
244
+ /**
245
+ * 生成 design.md 十三章节骨架字符串(纯函数不落盘,写盘/幂等保护由 design-init CLI 负责)。
246
+ * 章节标题与 brainstorm.js「design.md 必须包含的章节」模板逐字对齐(1 背景/2 设计目标/
247
+ * 3 非目标/4 拆分判断/5 总体方案/6 文件变更清单/7 接口定义/7.5 生命周期契约表/8 数据模型/
248
+ * 9 兼容策略/10 风险登记/11 决策追踪/12 自审),满足 stage-contract-spec 的字面命中检查
249
+ * (文件变更清单/风险登记/自审/生命周期豁免)与 plan.design-readiness 六章就绪检查。
250
+ * 决策追踪表从 decisionsText 当前版本 D 条目逐行预填;文件变更清单给表头+示例注释行
251
+ * (注释形态不会被 plan-postcheck 清单解析计入)。
252
+ * @param {{ changeName?: string, decisionsText?: string, author?: string, now?: string }} opts
253
+ * changeName=变更简述(入标题);decisionsText=decisions.md 全文;author/now=frontmatter 值
254
+ * @returns {string} design.md 骨架全文(LF 行尾,单一尾换行)
255
+ */
256
+ export function generateDesignSkeleton({ changeName, decisionsText, author, now } = {}) {
257
+ const change = String(changeName || '').trim() || '<变更简述>'
258
+ const decisions = parseDecisionDomains(decisionsText || '')
259
+
260
+ const lines = []
261
+ lines.push('---')
262
+ lines.push(`author: ${String(author || '').trim() || 'TODO(git 用户名)'}`)
263
+ lines.push(`created_at: ${String(now || '').trim() || 'TODO(ISO 时间)'}`)
264
+ lines.push('scale: large')
265
+ lines.push('---')
266
+ lines.push('')
267
+ lines.push(`# 设计文档(Design)— ${change}`)
268
+ lines.push('')
269
+ lines.push(`<!-- 由 sillyspec design-init 生成的骨架(${change})——逐节填散文后删除本注释;存量手写路径不受影响 -->`)
270
+ lines.push('')
271
+
272
+ lines.push('## 背景')
273
+ lines.push('')
274
+ lines.push('<!-- TODO:为什么做、解决什么问题 -->')
275
+ lines.push('')
276
+
277
+ lines.push('## 设计目标')
278
+ lines.push('')
279
+ lines.push('<!-- TODO:要达成什么 -->')
280
+ lines.push('')
281
+
282
+ lines.push('## 非目标')
283
+ lines.push('')
284
+ lines.push('<!-- TODO:明确不做的事(防止 scope creep) -->')
285
+ lines.push('')
286
+
287
+ lines.push('## 拆分判断')
288
+ lines.push('')
289
+ lines.push('<!-- TODO(如适用):为什么这样组织变更、为什么不走批量模式;不适用可整节删除 -->')
290
+ lines.push('')
291
+
292
+ lines.push('## 总体方案')
293
+ lines.push('')
294
+ lines.push('<!-- TODO:技术方案(分 Phase/Wave) -->')
295
+ lines.push('')
296
+
297
+ lines.push('## 文件变更清单')
298
+ lines.push('')
299
+ lines.push('| 操作 | 文件路径 | 说明 |')
300
+ lines.push('|---|---|---|')
301
+ lines.push('<!-- 示例行(替换为实际清单后删除本注释):| 新增 | src/xxx/NewFile.java | 说明(含对外字段时交代 producer→consumer 数据流) | -->')
302
+ lines.push('')
303
+
304
+ lines.push('## 接口定义')
305
+ lines.push('')
306
+ lines.push('<!-- TODO(代码类任务必填):方法签名、数据结构 -->')
307
+ lines.push('')
308
+
309
+ lines.push('## 生命周期契约表')
310
+ lines.push('')
311
+ lines.push('<!-- TODO:涉及 session/lease/agent_run/daemon/lifecycle/state transition/claim/heartbeat 等关键词时本表必填(事件×发起方×接收方×必需字段×状态变化 矩阵);确实不涉及时在紧邻位置写豁免短语——否定词必须紧邻「生命周期契约/lifecycle contract」,宽写法不被识别(写法见 brainstorm 模板) -->')
312
+ lines.push('')
313
+
314
+ lines.push('## 数据模型')
315
+ lines.push('')
316
+ lines.push('<!-- TODO(如涉及):表结构/字段变更;不涉及可整节删除或写明无 schema 变更 -->')
317
+ lines.push('')
318
+
319
+ lines.push('## 兼容策略(brownfield 必填)')
320
+ lines.push('')
321
+ lines.push('<!-- TODO(brownfield 必填):未配置新功能时行为不变 / 新旧逻辑的回退路径 / 不改变的 API 与表结构 -->')
322
+ lines.push('')
323
+
324
+ lines.push('## 风险登记')
325
+ lines.push('')
326
+ lines.push('| 编号 | 风险 | 等级 | 应对策略 |')
327
+ lines.push('|---|---|---|---|')
328
+ lines.push('| R-01 | (待填风险) | P1 | (待填应对策略) |')
329
+ lines.push('')
330
+
331
+ lines.push('## 决策追踪')
332
+ lines.push('')
333
+ lines.push('| 决策 | 覆盖点 | 状态 |')
334
+ lines.push('|---|---|---|')
335
+ if (decisions.length > 0) {
336
+ for (const { id } of decisions) lines.push(`| ${id} | (待填覆盖点) | 待确认 |`)
337
+ } else {
338
+ lines.push('<!-- 未从 decisions.md 解析出当前版本决策条目——如确无决策可写「无决策记录」 -->')
339
+ }
340
+ lines.push('<!-- TODO:说明每个 D-xxx@vN 被哪些 FR-xxx / 设计章节覆盖;标注仍未解决的 D-xxx@vN 或剩余风险 -->')
341
+ lines.push('')
342
+
343
+ lines.push('## 自审')
344
+ lines.push('')
345
+ lines.push('- [ ] 章节齐全(背景/设计目标/非目标/总体方案/文件变更清单/接口定义/风险登记)')
346
+ lines.push('- [ ] frontmatter 字段齐全(author/created_at/scale)')
347
+ lines.push('- [ ] 引用所有当前版本 D-xxx@vN')
348
+ lines.push('- [ ] 涉及生命周期关键词时含「生命周期契约表」或紧邻豁免短语')
349
+ lines.push('- [ ] UI 原型分级核对(涉前端文件时变更目录应有 prototype-*.html 或跳过原因记入风险登记)')
350
+ lines.push('- [ ] 不确定的问题标注「⚠️ 自审存疑」')
351
+ lines.push('')
352
+
353
+ return lines.join('\n')
354
+ }
355
+
356
+
357
+ // ---------------------------------------------------------------------------
358
+ // design.md 文件清单行级核验(change: 2026-09-07-ir-hardening,D-004@v1,FR-02)
359
+ // ---------------------------------------------------------------------------
360
+
361
+ /** 剥路径中的 <...> 占位段(`docs/<project>/scan` → `docs/`;占位形态对齐 collectDocRefs 层1 容差先例) */
362
+ function stripPathPlaceholders(p) {
363
+ return String(p || '').replace(/<[^/>]*>/g, '').replace(/\/{2,}/g, '/').replace(/\/$/, '')
364
+ }
365
+
366
+ /**
367
+ * validateDesignFileList —— design.md 文件变更清单逐条存在性核验(幻觉路径 gate)。
368
+ *
369
+ * 分级(design D-004 + Grill P2-⑤):
370
+ * - `NEW:` 前缀(解析器不剥前缀,startsWith 直判)→ 通过(计划新建文件豁免)
371
+ * - 路径存在(cwd 相对 existsSync;判前剥 <...> 占位段)→ 通过
372
+ * - 含 glob 字符(* / ?)→ warning 跳过(pathMatches 体系本支持 glob 合法形态,existsSync 恒 false 会假 ERROR——fail-soft)
373
+ * - 其余 → errors { path, message }(信封 code design_file_ref_invalid)
374
+ * - design.md 不存在/无清单段/解析异常 → warnings(small 变更可无清单;fail-soft 不阻断)
375
+ *
376
+ * 范围:keepSillyspecDocs=true——`.sillyspec/docs/**` 交付物路径在核验范围内(其余 .sillyspec/ 条目解析器本就丢弃)。
377
+ * 纯函数不落盘。
378
+ *
379
+ * @param {{ changeDir: string, cwd: string }} opts
380
+ * @returns {{ ok: boolean, errors: Array<{path, message}>, warnings: string[] }}
381
+ */
382
+ export function validateDesignFileList({ changeDir, cwd } = {}) {
383
+ const errors = []
384
+ const warnings = []
385
+ try {
386
+ if (!changeDir || !cwd) {
387
+ return { ok: true, errors, warnings: ['design 清单核验跳过:缺少 changeDir/cwd'] }
388
+ }
389
+ const designPath = join(changeDir, 'design.md')
390
+ if (!existsSync(designPath)) {
391
+ return { ok: true, errors, warnings: ['design 清单核验跳过:design.md 不存在'] }
392
+ }
393
+ const entries = parseFileChangeListDetailed(designPath, { keepSillyspecDocs: true })
394
+ if (!Array.isArray(entries) || entries.length === 0) {
395
+ return { ok: true, errors, warnings: ['design 清单核验跳过:design.md 无文件变更清单段(small 变更可无清单)'] }
396
+ }
397
+ for (const e of entries) {
398
+ const raw = String((e && e.path) || '').trim()
399
+ if (!raw) continue
400
+ if (raw.startsWith('NEW:')) continue // 计划新建豁免(与 validateTargetFiles 同语义)
401
+ if (raw.includes('*') || raw.includes('?')) {
402
+ warnings.push(`design 清单条目含 glob 字符跳过存在性核验:${raw}`)
403
+ continue
404
+ }
405
+ const normalized = stripPathPlaceholders(raw)
406
+ if (!normalized || normalized === '.') continue
407
+ if (!existsSync(join(cwd, normalized))) {
408
+ errors.push({
409
+ path: raw,
410
+ message: `design_file_ref_invalid:文件变更清单条目「${raw}」既不存在也无 NEW: 前缀(幻觉路径/书写错误)——修正路径,或计划新建的文件改为 NEW:${raw} 前缀`,
411
+ })
412
+ }
413
+ }
414
+ return { ok: errors.length === 0, errors, warnings }
415
+ } catch (e) {
416
+ return { ok: true, errors, warnings: [`design 清单核验异常 fail-soft 放行:${e && e.message ? e.message : e}`] }
417
+ }
418
+ }
@@ -234,11 +234,23 @@ dispatch 后在当前 step 内主动轮询,不阻塞等待:
234
234
 
235
235
  1. **轮询 \`list_workers\`**:间隔默认 15s,可配 local.yaml \`dispatch.poll_interval_ms\`
236
236
  - 入参 \`mission_id\`;关注每个 worker 的 status(pending → running → completed/failed)
237
- 2. **per-worker 超时**:默认可配 local.yaml \`dispatch.worker_timeout_ms\`;超时 → 三步兜底:
238
- - a. **标记**:调 \`report_progress\` tool(worker_id + 超时说明)记录该 worker 超时
239
- - b. **kill lease 防双写**:调 \`report_progress\` tool(worker_id + \`kill: true\` + marker)终止 lease
240
- (等价 client.js \`killLease(workerId)\` 路径;SillyHub 当前无专用 kill tool,路径A 落地后升级为专用 kill)
241
- - c. **fallback Local 重派**:该 task 改用本机 Agent tool 重派(调 \`sillyspec dispatch hint\` 走 Local 模板)
237
+ 2. **worker 活性知情决策(2026-09-07-agent-liveness-states task-13)**:\`list_workers\`
238
+ 返回的 worker 在 **running 期间** 附 \`liveness\` 字段(\`{ state, evidence, derived_at }\`,
239
+ 无则为 null)——\`state\` 取值 \`working\` / \`blocked\` / \`idle\` / \`ended\` / \`unknown\`
240
+ (daemon liveness 推导;\`blocked\` 主源为第一方权限事件,PERMISSION_REQUEST 等人)。
241
+ 轮询时按活性分流,替代无脑超时 kill:
242
+ - a. **\`liveness.state=blocked\` 且持续超阈值**(对齐平台 \`BLOCKED_ALERT_MS\`=120s 口径,
243
+ 连续两轮轮询仍 blocked 即视为持续)→ **升级给人**:告知用户该 worker 在等权限确认
244
+ (harness + evidence),由人处理(平台会同步发 agent_blocked 通知);**绝不 kill、
245
+ 绝不自动批准**(自动批准权限提示=代人做安全决策,安全敏感)。
246
+ - b. **\`liveness.state=working\` 且久无终态** → 属长任务场景(日志持续增长、跑测试等),
247
+ 按既有 \`dispatch.worker_timeout_ms\` 继续等待,**不抢跑 kill**——区分「在干活的长任务」
248
+ 与「死了」正是活性字段的价值。
249
+ - c. **仅当超时且无 liveness / \`liveness.state=unknown\`** → 走既有三步兜底(超时兜底路径):
250
+ - ① **标记**:调 \`report_progress\` tool(worker_id + 超时说明)记录该 worker 超时
251
+ - ② **kill lease 防双写**:调 \`report_progress\` tool(worker_id + \`kill: true\` + marker)终止 lease
252
+ (等价 client.js \`killLease(workerId)\` 路径;SillyHub 当前无专用 kill tool,路径A 落地后升级为专用 kill)
253
+ - ③ **fallback Local 重派**:该 task 改用本机 Agent tool 重派(调 \`sillyspec dispatch hint\` 走 Local 模板)
242
254
  3. **worker 终态(completed / failed / killed)** → 进入下方回收约定
243
255
 
244
256
  ${recycleRule}`