sillyspec 3.28.10 → 3.28.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/sillyspec-execute/SKILL.md +223 -222
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.claude/skills/sillyspec-quick/SKILL.md +8 -10
- package/.claude/skills/sillyspec-verify/SKILL.md +91 -87
- package/.husky/pre-commit +4 -0
- package/.husky/pre-push +3 -0
- package/AGENTS.md +1 -1
- package/package.json +1 -1
- package/src/change-risk-profile.js +76 -6
- package/src/commit-guard.js +119 -0
- package/src/config-schema.js +23 -0
- package/src/design-facts.js +46 -9
- package/src/diagnostic-codes.js +95 -0
- package/src/docs-check.js +14 -10
- package/src/docs-debt.js +7 -2
- package/src/docs-gate.js +42 -15
- package/src/doctor-diagnostics.js +141 -1
- package/src/endpoint-extractor.js +50 -2
- package/src/foreign-declared.js +25 -16
- package/src/hooks/worktree-guard.js +75 -2
- package/src/index.js +129 -1
- package/src/machine-interface.js +63 -0
- package/src/probe7-anchor-check.js +20 -2
- package/src/progress/stage-machine.js +34 -5
- package/src/progress.js +1 -1
- package/src/quicklog.js +231 -1
- package/src/run/complete-handlers.js +172 -1
- package/src/run/complete.js +73 -3
- package/src/run/gate-snapshot.js +262 -0
- package/src/run/gates.js +41 -7
- package/src/run/prompt.js +79 -1
- package/src/run/shared.js +10 -0
- package/src/run/verify-quality-scan.js +151 -5
- package/src/stage-contract-spec.js +14 -0
- package/src/stage-contract.js +738 -8
- package/src/stage-review-checklist.js +16 -2
- package/src/stage-review.js +137 -2
- package/src/stages/archive.js +9 -5
- package/src/stages/brainstorm.js +7 -1
- package/src/stages/execute.js +62 -8
- package/src/stages/plan-postcheck.js +42 -5
- package/src/stages/plan.js +8 -1
- package/src/stages/quick.js +5 -5
- package/src/stages/verify.js +8 -3
- package/src/sync.js +69 -0
- package/src/task-review.js +9 -3
- package/src/verify-facts-schema.js +95 -10
- package/src/verify-postcheck.js +346 -28
- package/src/verify-probes.js +1221 -46
- package/src/worktree-apply.js +175 -3
- package/src/write-audit.js +55 -0
- package/.claude/full.json +0 -1
package/src/run/gate-snapshot.js
CHANGED
|
@@ -111,6 +111,242 @@ function smokeImportPython(pythonBin, snapshotRoot, relFile) {
|
|
|
111
111
|
return null
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
/**
|
|
115
|
+
* local.yaml `gate_snapshot:` 段 `copy:` 列表的轻量行扫描(与 verify-postcheck
|
|
116
|
+
* extractLintCommand 同风格——单键定向取值,不引 js-yaml,本文件零新依赖)。
|
|
117
|
+
*
|
|
118
|
+
* 认块列表(缩进 `- <path>` 条目)与 inline flow(`copy: [a, b]`)两形态(对齐
|
|
119
|
+
* worktree.supplyFiles 等既有数组键的「块列表或 inline flow 均可解析」约定);空行/注释行
|
|
120
|
+
* 跳过不破段;离开 gate_snapshot 段(缩进回收到段头及以下 / 新顶层键)即止。容错:值剥
|
|
121
|
+
* 尾注(` #…`)与成对引号。
|
|
122
|
+
* @param {string} yamlText local.yaml 原文
|
|
123
|
+
* @returns {string[]} copy 条目原始值(未规整,交 applyGateSnapshotCopy 逐条规整)
|
|
124
|
+
*/
|
|
125
|
+
function _stripYamlValue(v) {
|
|
126
|
+
let s = String(v).trim()
|
|
127
|
+
const hash = s.indexOf(' #')
|
|
128
|
+
if (hash >= 0) s = s.slice(0, hash).trim()
|
|
129
|
+
if ((s.startsWith('"') && s.endsWith('"') && s.length >= 2) || (s.startsWith("'") && s.endsWith("'") && s.length >= 2)) {
|
|
130
|
+
s = s.slice(1, -1).trim()
|
|
131
|
+
}
|
|
132
|
+
return s
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function parseGateSnapshotCopy(yamlText) {
|
|
136
|
+
if (!yamlText) return []
|
|
137
|
+
const out = []
|
|
138
|
+
let inGate = false // 位于 gate_snapshot: 段内
|
|
139
|
+
let gateIndent = -1
|
|
140
|
+
let inCopy = false // 位于段内 copy: 列表内
|
|
141
|
+
let copyIndent = -1
|
|
142
|
+
for (const raw of String(yamlText).split(/\r?\n/)) {
|
|
143
|
+
const content = raw.trim()
|
|
144
|
+
if (!content || content.startsWith('#')) continue // 空行/注释行不破段
|
|
145
|
+
const indent = raw.match(/^[ \t]*/)[0].length
|
|
146
|
+
if (!inGate) {
|
|
147
|
+
if (/^gate_snapshot:\s*(?:#.*)?$/.test(content)) { inGate = true; gateIndent = indent; inCopy = false }
|
|
148
|
+
continue
|
|
149
|
+
}
|
|
150
|
+
if (indent <= gateIndent) { inGate = false; inCopy = false; continue } // 出段(新顶层键)
|
|
151
|
+
if (!inCopy) {
|
|
152
|
+
const flow = content.match(/^copy:\s*\[([^\]]*)\]/)
|
|
153
|
+
if (flow) {
|
|
154
|
+
for (const part of flow[1].split(',')) {
|
|
155
|
+
const v = _stripYamlValue(part)
|
|
156
|
+
if (v) out.push(v)
|
|
157
|
+
}
|
|
158
|
+
continue // inline flow 当行自洽,继续扫段内后续内容
|
|
159
|
+
}
|
|
160
|
+
if (/^copy:\s*(?:#.*)?$/.test(content)) { inCopy = true; copyIndent = indent }
|
|
161
|
+
continue // 段内其它子键忽略
|
|
162
|
+
}
|
|
163
|
+
if (indent < copyIndent) { inCopy = false; continue } // 缩进回收出列表
|
|
164
|
+
const item = content.match(/^-\s*(.+)$/)
|
|
165
|
+
if (!item) { inCopy = false; continue } // 同层非列表行 = 列表结束
|
|
166
|
+
const v = _stripYamlValue(item[1])
|
|
167
|
+
if (v) out.push(v)
|
|
168
|
+
}
|
|
169
|
+
return out
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* local.yaml `gate_snapshot:` 段 `commands:` 列表扫描(2026-09-17-feedback-hardening
|
|
174
|
+
* R1 / FR-01 / D-002@v2,坑 gate-snapshot-missing-generated-artifacts 二阶——copy 面只搬
|
|
175
|
+
* 「主仓现有态」,主仓侧生成物缺失(fresh clone 未跑 postinstall)或过期(build-id 类
|
|
176
|
+
* 构建期产物随版本变)时供给缺失/陈旧态,快照内全量 lint/test 必挂,用户 2026-09-17
|
|
177
|
+
* 负面①实证)。与 parseGateSnapshotCopy 同风格段内单键扫描(本文件零新依赖,不引
|
|
178
|
+
* js-yaml);认块列表(缩进 `- <cmd>` 条目)与 inline flow(`commands: [a, b]`)双形态
|
|
179
|
+
*(对齐 supplyFiles/copy 既有数组键约定);空行/注释行不破段;缩进回收到段头及以下/
|
|
180
|
+
* 新顶层键即出段——段外同名键(顶层 `commands:` 命令段 / 其它段内嵌 commands)不误收。
|
|
181
|
+
* @param {string} yamlText local.yaml 原文
|
|
182
|
+
* @returns {string[]} 命令串(未配置/空段/读失败 → [])
|
|
183
|
+
*/
|
|
184
|
+
export function parseGateSnapshotCommands(yamlText) {
|
|
185
|
+
if (!yamlText) return []
|
|
186
|
+
const out = []
|
|
187
|
+
let inGate = false // 位于 gate_snapshot: 段内
|
|
188
|
+
let gateIndent = -1
|
|
189
|
+
let inCmds = false // 位于段内 commands: 列表内
|
|
190
|
+
let cmdsIndent = -1
|
|
191
|
+
for (const raw of String(yamlText).split(/\r?\n/)) {
|
|
192
|
+
const content = raw.trim()
|
|
193
|
+
if (!content || content.startsWith('#')) continue // 空行/注释行不破段
|
|
194
|
+
const indent = raw.match(/^[ \t]*/)[0].length
|
|
195
|
+
if (!inGate) {
|
|
196
|
+
if (/^gate_snapshot:\s*(?:#.*)?$/.test(content)) { inGate = true; gateIndent = indent; inCmds = false }
|
|
197
|
+
continue
|
|
198
|
+
}
|
|
199
|
+
if (indent <= gateIndent) { inGate = false; inCmds = false; continue } // 出段(新顶层键)
|
|
200
|
+
if (!inCmds) {
|
|
201
|
+
const flow = content.match(/^commands:\s*\[([^\]]*)\]/)
|
|
202
|
+
if (flow) {
|
|
203
|
+
for (const part of flow[1].split(',')) {
|
|
204
|
+
const v = _stripYamlValue(part)
|
|
205
|
+
if (v) out.push(v)
|
|
206
|
+
}
|
|
207
|
+
continue // inline flow 当行自洽,继续扫段内后续内容
|
|
208
|
+
}
|
|
209
|
+
if (/^commands:\s*(?:#.*)?$/.test(content)) { inCmds = true; cmdsIndent = indent }
|
|
210
|
+
continue // 段内其它子键(copy 等)忽略
|
|
211
|
+
}
|
|
212
|
+
if (indent < cmdsIndent) { inCmds = false; continue } // 缩进回收出列表
|
|
213
|
+
const item = content.match(/^-\s*(.+)$/)
|
|
214
|
+
if (!item) { inCmds = false; continue } // 同层非列表行 = 列表结束
|
|
215
|
+
const v = _stripYamlValue(item[1])
|
|
216
|
+
if (v) out.push(v)
|
|
217
|
+
}
|
|
218
|
+
return out
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* gate_snapshot.commands 执行段(D-002@v2):快照构建期在快照根逐条执行生成物命令,产出
|
|
223
|
+
* 「本仓应然态」。调用契约:cwd=**快照根**(环境目录 junction 已就位;命令产出真实文件
|
|
224
|
+
* → 后续 copy 面对已存在 dst 跳过,新鲜度优先)。每条 300s 超时帽;非零退出/超时只
|
|
225
|
+
* warn(含 cmd 与输出尾 ≤300 字)继续下一条——fail-open 与 copy 面同策略,不作废快照;
|
|
226
|
+
* 执行结果不落 meta(快照即用即弃,console 输出即审计面)。信任级与
|
|
227
|
+
* commands.install/test/lint 同:主仓 local.yaml 用户自持配置,不加命令白名单。
|
|
228
|
+
* 内部导出(createGateSnapshot 唯一运行时消费方;直测断言返回值)。
|
|
229
|
+
* @param {string} snapshotRoot 快照根(命令 cwd)
|
|
230
|
+
* @param {string[]} commands 命令串列表(parseGateSnapshotCommands 产物)
|
|
231
|
+
* @returns {{ ran: number, failed: Array<{cmd: string, reason: string}> }} ran=实际执行条数(含失败),failed=逐条失败记录
|
|
232
|
+
*/
|
|
233
|
+
export function runGateSnapshotCommands(snapshotRoot, commands) {
|
|
234
|
+
if (!Array.isArray(commands) || commands.length === 0) return { ran: 0, failed: [] }
|
|
235
|
+
let ran = 0
|
|
236
|
+
const failed = []
|
|
237
|
+
for (const raw of commands) {
|
|
238
|
+
const cmd = String(raw).trim()
|
|
239
|
+
if (!cmd) continue
|
|
240
|
+
ran++
|
|
241
|
+
let r = null
|
|
242
|
+
try {
|
|
243
|
+
r = spawnSync(cmd, { shell: true, cwd: snapshotRoot, timeout: 300_000, encoding: 'utf8' })
|
|
244
|
+
} catch (e) {
|
|
245
|
+
const reason = `spawn 异常:${e && e.message ? e.message : e}`
|
|
246
|
+
failed.push({ cmd, reason })
|
|
247
|
+
console.warn(`⚠️ gate_snapshot.commands 条目失败(${reason},不作废快照):${cmd}`)
|
|
248
|
+
continue
|
|
249
|
+
}
|
|
250
|
+
const timedOut = r && r.error && (r.error.code === 'ETIMEDOUT' || /ETIMEDOUT/i.test(String(r.error.message || '')))
|
|
251
|
+
if (r.error || r.status !== 0) {
|
|
252
|
+
const reason = timedOut
|
|
253
|
+
? '超时(>300s 帽)'
|
|
254
|
+
: (r.error ? `spawn 失败:${r.error.message}` : `非零退出码 ${r.status}`)
|
|
255
|
+
failed.push({ cmd, reason })
|
|
256
|
+
const tail = String((r.stdout || '') + (r.stderr || '')).trim().slice(-300)
|
|
257
|
+
console.warn(`⚠️ gate_snapshot.commands 条目失败(${reason},不作废快照继续下一条):${cmd}${tail ? `|输出尾:${tail}` : ''}`)
|
|
258
|
+
continue
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return { ran, failed }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** 目录递归复制(copy 面 junction 不可用时的回退)。符号链接条目跳过(生成物目录内不追链)。 */
|
|
265
|
+
function copyDirRecursive(srcDir, dstDir) {
|
|
266
|
+
mkdirSync(dstDir, { recursive: true })
|
|
267
|
+
for (const e of readdirSync(srcDir, { withFileTypes: true })) {
|
|
268
|
+
const s = join(srcDir, e.name)
|
|
269
|
+
const d = join(dstDir, e.name)
|
|
270
|
+
if (e.isDirectory()) copyDirRecursive(s, d)
|
|
271
|
+
else if (e.isFile()) copyFileSync(s, d)
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* gate_snapshot.copy 面(2026-09-16-friction5-hardening R4 / FR-04 / D-002@v1,坑
|
|
277
|
+
* gate-snapshot-missing-generated-artifacts——用户 2026-09-16 驾驭小结④实证只能
|
|
278
|
+
* SNAPSHOT_OFF 对照):HEAD 快照缺 gitignored 生成物(api-types/generated 类不进 HEAD
|
|
279
|
+
* 也不在会话文件集)→ 快照内 lint/test 环境性假败。按主仓 local.yaml 的 copy 清单把
|
|
280
|
+
* 生成物从主仓 junction 链接进快照(失败回退复制),消除环境性假败。
|
|
281
|
+
*
|
|
282
|
+
* 调用契约:cwd=**主仓**根(local.yaml 从主仓读——快照内 local.yaml 是稍后复制的复制件,
|
|
283
|
+
* copy 面必须先于复制生效);在 overlay 之后调用(dst 已存在 = overlay 已覆盖本变更最新态,
|
|
284
|
+
* 跳过不覆盖)。
|
|
285
|
+
*
|
|
286
|
+
* 逐条容错(fail-open,与本文件快照基建策略一致,任何失败不作废快照):路径规整
|
|
287
|
+
*(String、反斜杠→正斜杠、trim);含 '..' 或绝对路径形态(POSIX / 与 Windows 盘符)拒绝
|
|
288
|
+
* warn 跳过;主仓不存在 warn 跳过;junction 抛错回退递归 copy(目录)/ copyFileSync(文件);
|
|
289
|
+
* 再失败 warn 跳过。成功 ≥1 条 console.log 一行报备。未配置/空清单/读失败 → 全段空转
|
|
290
|
+
* 零输出零行为(存量 local.yaml 逐字节不变)。
|
|
291
|
+
*
|
|
292
|
+
* ⚠️ junction 是活链接:快照内再跑生成命令会写穿到主仓该目录(constraints 明示,
|
|
293
|
+
* config-schema desc 与 renderExample 注释同步警告)。
|
|
294
|
+
*
|
|
295
|
+
* 新增内部导出(symbol-impact:内部测试直测边界条目用;createGateSnapshot 唯一运行时消费方)。
|
|
296
|
+
* @param {string} cwd 主仓根
|
|
297
|
+
* @param {string} snapshotRoot 快照根
|
|
298
|
+
* @returns {number} 成功链接/复制的条目数
|
|
299
|
+
*/
|
|
300
|
+
export function applyGateSnapshotCopy(cwd, snapshotRoot) {
|
|
301
|
+
let entries = []
|
|
302
|
+
try {
|
|
303
|
+
const cfgPath = join(cwd, '.sillyspec', 'local.yaml')
|
|
304
|
+
if (!existsSync(cfgPath)) return 0
|
|
305
|
+
entries = parseGateSnapshotCopy(readFileSync(cfgPath, 'utf8'))
|
|
306
|
+
} catch { return 0 } // 读侧容错:解析/读失败与未配置同兜底(空转,不连坐快照)
|
|
307
|
+
if (!Array.isArray(entries) || entries.length === 0) return 0
|
|
308
|
+
let linked = 0
|
|
309
|
+
for (const raw of entries) {
|
|
310
|
+
const p = String(raw).replace(/\\/g, '/').trim()
|
|
311
|
+
if (!p) continue
|
|
312
|
+
if (p.includes('..') || p.startsWith('/') || /^[A-Za-z]:\//.test(p)) {
|
|
313
|
+
console.warn(`⚠️ gate_snapshot.copy 条目「${p}」含 .. 或绝对路径形态,拒绝(快照外写面)——跳过`)
|
|
314
|
+
continue
|
|
315
|
+
}
|
|
316
|
+
let srcStat = null
|
|
317
|
+
try { srcStat = statSync(join(cwd, p)) } catch { /* 主仓不存在,下方 warn 跳过 */ }
|
|
318
|
+
if (!srcStat) {
|
|
319
|
+
console.warn(`⚠️ gate_snapshot.copy 条目主仓不存在,跳过:${p}`)
|
|
320
|
+
continue
|
|
321
|
+
}
|
|
322
|
+
const src = join(cwd, p)
|
|
323
|
+
const dst = join(snapshotRoot, p)
|
|
324
|
+
if (existsSync(dst)) continue // overlay 已覆盖(本变更最新态优先)
|
|
325
|
+
try {
|
|
326
|
+
mkdirSync(dirname(dst), { recursive: true })
|
|
327
|
+
let viaLink = false
|
|
328
|
+
try {
|
|
329
|
+
symlinkSync(src, dst, 'junction')
|
|
330
|
+
// Windows 文件 junction 可「建成但不解析」(重解析点指向非目录,symlinkSync 不抛、
|
|
331
|
+
// existsSync 为假)——链接后验真,假成功回退复制(平台差异实测坑)
|
|
332
|
+
viaLink = existsSync(dst)
|
|
333
|
+
} catch { viaLink = false }
|
|
334
|
+
if (!viaLink) {
|
|
335
|
+
try { rmSync(dst, { recursive: true, force: true }) } catch { /* 假链接残留清不掉 → 下方复制报错走 warn */ }
|
|
336
|
+
if (srcStat.isDirectory()) copyDirRecursive(src, dst)
|
|
337
|
+
else copyFileSync(src, dst)
|
|
338
|
+
}
|
|
339
|
+
linked++
|
|
340
|
+
} catch (e) {
|
|
341
|
+
console.warn(`⚠️ gate_snapshot.copy 条目「${p}」junction/复制均失败:${e && e.message ? e.message : e}——跳过(不作废快照)`)
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (linked > 0) {
|
|
345
|
+
console.log(`🔬 门禁快照 copy 面:${linked} 个 gate_snapshot.copy 条目自主仓 junction/复制进快照(⚠️ junction 是活链接,快照内再跑生成命令会写穿到主仓该目录)`)
|
|
346
|
+
}
|
|
347
|
+
return linked
|
|
348
|
+
}
|
|
349
|
+
|
|
114
350
|
/**
|
|
115
351
|
* 创建隔离快照:HEAD worktree + 会话文件 overlay + node_modules junction + local.yaml。
|
|
116
352
|
* @param {{ cwd: string, files: string[] }} opts cwd=主仓根;files=本会话变更文件(仓库根相对 POSIX)
|
|
@@ -191,6 +427,32 @@ export function createGateSnapshot({ cwd, files, sourceRoot = null, skipImportSm
|
|
|
191
427
|
return null
|
|
192
428
|
}
|
|
193
429
|
|
|
430
|
+
// gate_snapshot.commands 命令面(D-002@v2,2026-09-17-feedback-hardening R1,用户
|
|
431
|
+
// 2026-09-17 负面①实证「全量快照必挂」):主仓侧生成物缺失(fresh clone 未跑
|
|
432
|
+
// postinstall)或过期时,copy 面只搬「主仓现有态」供给缺失/陈旧态——本段在环境链接
|
|
433
|
+
// 就位后、copy 面前于快照根逐条执行 postinstall 类生成命令,产出「本仓应然态」(命令
|
|
434
|
+
// 产出真实文件 → copy 面对已存在 dst 跳过,新鲜度优先)。配置恒从主仓 cwd 读原文
|
|
435
|
+
//(local.yaml 复制段在后面,快照内副本不采信——与 copy 键同源)。未配置 → 全段空转
|
|
436
|
+
// 零输出(存量快照逐字节不变);整段 try/catch fail-open:任何异常只 warn 不作废快照。
|
|
437
|
+
try {
|
|
438
|
+
const gsCfgPath = join(cwd, '.sillyspec', 'local.yaml')
|
|
439
|
+
const gsCommands = existsSync(gsCfgPath) ? parseGateSnapshotCommands(readFileSync(gsCfgPath, 'utf8')) : []
|
|
440
|
+
if (gsCommands.length > 0) {
|
|
441
|
+
console.log(`🔬 门禁快照 commands 面:${gsCommands.length} 条 gate_snapshot.commands 在快照内逐条执行(cwd=快照根,产出本仓应然态生成物)`)
|
|
442
|
+
console.warn(`⚠️ 环境目录是活链接:命令不得改写 node_modules/venv 等环境目录(写会穿透到主仓)——只放生成物命令(npm run gen:build-id 类)`)
|
|
443
|
+
runGateSnapshotCommands(snapshotRoot, gsCommands)
|
|
444
|
+
}
|
|
445
|
+
} catch (e) {
|
|
446
|
+
console.warn(`⚠️ gate_snapshot.commands 执行段异常(不作废快照):${e && e.message ? e.message : e}`)
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// gate_snapshot.copy 面(R4 / D-002@v1,2026-09-16-friction5-hardening):gitignored
|
|
450
|
+
// 生成物从主仓 junction/复制进快照。位置=环境目录链接段之后(环境优先级最高)、
|
|
451
|
+
// local.yaml 复制段之前(从主仓 cwd 读原文——快照内 local.yaml 是稍后才复制的复制件,
|
|
452
|
+
// 且 copy 面必须先于复制生效);overlay 已在上面完成(dst 已存在即跳过,本变更最新态优先)。
|
|
453
|
+
// 未配置 gate_snapshot.copy → 全段空转零行为(见 applyGateSnapshotCopy jsdoc)。
|
|
454
|
+
applyGateSnapshotCopy(cwd, snapshotRoot)
|
|
455
|
+
|
|
194
456
|
// local.yaml(gitignore 不进 HEAD;门禁命令配置来源)+ package-lock 保持 HEAD 版(npm test 不装新依赖)
|
|
195
457
|
for (const cfg of [join('.sillyspec', 'local.yaml')]) {
|
|
196
458
|
const src = join(cwd, cfg)
|
package/src/run/gates.js
CHANGED
|
@@ -919,9 +919,9 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
919
919
|
if (existsSync(p7ReportPath)) {
|
|
920
920
|
const p7Anchor = checkProbe7AnchorCoverage(readFileSync(p7ReportPath, 'utf8'))
|
|
921
921
|
if (p7Anchor.applicable && p7Anchor.missingAnchors.length > 0) {
|
|
922
|
-
console.warn(`\n⚠️ 探针7 锚点校验:${p7Anchor.missingAnchors.length}/${p7Anchor.coveredRows} 行判定为 covered 但证据列缺 file:line
|
|
922
|
+
console.warn(`\n⚠️ 探针7 锚点校验:${p7Anchor.missingAnchors.length}/${p7Anchor.coveredRows} 行判定为 covered 但证据列缺 file:line / \`.test.\` 文件名 / 反引号路径锚点(advisory 不阻断,审查会要求回补):`)
|
|
923
923
|
for (const m of p7Anchor.missingAnchors.slice(0, 10)) {
|
|
924
|
-
console.warn(` - ${m.task}:${m.acceptance || '(空 acceptance)'}——证据当前为「${m.evidence || '(空)'}」,应给 \`测试文件路径:行号\`
|
|
924
|
+
console.warn(` - ${m.task}:${m.acceptance || '(空 acceptance)'}——证据当前为「${m.evidence || '(空)'}」,应给 \`测试文件路径:行号\` 首命中锚点、\`.test.\` 测试文件名或反引号包裹的路径/测试名(行号可省)`)
|
|
925
925
|
}
|
|
926
926
|
if (p7Anchor.missingAnchors.length > 10) console.warn(` …还有 ${p7Anchor.missingAnchors.length - 10} 行`)
|
|
927
927
|
}
|
|
@@ -1051,8 +1051,14 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
1051
1051
|
// 不在自动刷新范围,照常 fail-closed。
|
|
1052
1052
|
const gated = validateStageReviewWithAutoRefresh({ stage: stageName, reviewType, runtimeRoot, reviewRunId, searchDirs, autoRefresh: true })
|
|
1053
1053
|
if (gated.autoRefreshed) {
|
|
1054
|
-
console.warn(`\n🔄 stage review docHash
|
|
1054
|
+
console.warn(`\n🔄 stage review docHash 失配已自动刷新(第 ${gated.refreshOrdinal || 1} 次;hash ${gated.hashDrift || 'n/a'};主文档改版后机械重算,verdict/checklist 保留——结论是否仍适用于新文档需人工确认)`)
|
|
1055
1055
|
console.warn(` 已就地更新: ${gated.refreshedPath}`)
|
|
1056
|
+
// C3 升级提醒(2026-09-15 wp EHS 实证:NEW: 批注/需求调整/Reverse Sync 多次走此通道):
|
|
1057
|
+
// 同一 review 被反复刷新 = 主文档在审查通过后反复变更,「审查后的变更不再见审查」的
|
|
1058
|
+
// 面积在累积——序数 ≥2 从一句注记升级为显式人工确认提醒(仍放行,不伪造重审)。
|
|
1059
|
+
if ((gated.refreshOrdinal || 1) >= 2) {
|
|
1060
|
+
console.warn(` ⚠️ 该 review 已第 ${gated.refreshOrdinal} 次刷新——主文档在审查后反复变更,建议人工复核 verdict 结论续用性(涉及设计要点/接口变更时应重审,勿把自动放行当「新文档已审过」)`)
|
|
1061
|
+
}
|
|
1056
1062
|
}
|
|
1057
1063
|
const reviewResult = gated.result
|
|
1058
1064
|
printStageReviewResult(reviewResult, { stage: stageName, reviewRunId, runtimeRoot, changeName })
|
|
@@ -1065,7 +1071,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
1065
1071
|
// channel 识别(2026-09-10 通道优先序批次):reviewer.channel 结构化落款优先,兼容
|
|
1066
1072
|
// reviewerNotes 首行「降级:」约定;platform 通道(P2 review-dispatch 落地后)ℹ️ 留痕
|
|
1067
1073
|
// missionId 供追溯,其余通道正常放行。
|
|
1068
|
-
const { classifyReviewerChannel } = await import('../stage-review.js')
|
|
1074
|
+
const { classifyReviewerChannel, hasDelegatedWriteDisclosure } = await import('../stage-review.js')
|
|
1069
1075
|
const reviewChannel = classifyReviewerChannel(reviewResult.review)
|
|
1070
1076
|
if (reviewChannel === 'self') {
|
|
1071
1077
|
console.warn(`\n⚠️ Stage Review 降级自审(tier=independent 但宿主无 Agent tool):${stageName} review 按 prompt 降级条款由当前 agent 自审产出——独立性折损已留痕(reviewerNotes 首行「降级:」),结论应附源码锚点补偿,建议人工抽查关键结论。`)
|
|
@@ -1073,6 +1079,12 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
1073
1079
|
const mid = reviewResult.review && reviewResult.review.reviewer && reviewResult.review.reviewer.missionId
|
|
1074
1080
|
console.log(`\nℹ️ Stage Review 平台派发(channel=platform${mid ? `,missionId=${mid}` : ',missionId 未落款'}):${stageName} review 由平台独立 worker 产出(独立进程/会话)。`)
|
|
1075
1081
|
}
|
|
1082
|
+
// C4 代落盘审计行(2026-09-15 wp EHS 实证:平台写通道故障下主流程代子代理落盘 review,
|
|
1083
|
+
// 独立性折损当时只在 notes 里、gate 不可见):reviewerNotes 首行「代落盘:」披露 → ⚠️ 留痕
|
|
1084
|
+
// (与降级自审同哲学:可见的折损优于静默伪装;首行约定识别,不阻断)。
|
|
1085
|
+
if (hasDelegatedWriteDisclosure(reviewResult.review)) {
|
|
1086
|
+
console.warn(`\n⚠️ Stage Review 代落盘披露:${stageName} review 的 reviewerNotes 首行标记「代落盘」——review.json 由主代理代(写通道故障的)审查者落盘,独立性依赖审查者结论回传的忠实性,建议人工抽查关键结论与回传文本一致性。`)
|
|
1087
|
+
}
|
|
1076
1088
|
}
|
|
1077
1089
|
} catch (e) {
|
|
1078
1090
|
// fail-closed:Gate 自身异常阻断完成,不静默放行(与 Task Review Gate 一致)
|
|
@@ -1653,11 +1665,33 @@ export function printReconcileTargetFilesCheck(r, envelope = buildReconcileTarge
|
|
|
1653
1665
|
// ③类(做了没声明=scope creep):WARNING 放行,suspectTask 尽力归因随行列出
|
|
1654
1666
|
if (r.status === 'undeclared') {
|
|
1655
1667
|
console.warn(`\n⚠️ target_files 对账发现 ${r.undeclared.length} 个实际改动未在任何 task 卡声明(③类 scope creep,advisory 不阻断):`)
|
|
1656
|
-
|
|
1668
|
+
if (r.undeclared.length <= 20) {
|
|
1669
|
+
for (const u of r.undeclared) {
|
|
1657
1670
|
console.warn(` - ${u.path}${u.suspectTask ? `(疑似 ${u.suspectTask} 的改动——该 task 的 review.json changedFiles 提及)` : ''}`)
|
|
1658
1671
|
}
|
|
1659
|
-
|
|
1660
|
-
|
|
1672
|
+
} else {
|
|
1673
|
+
// >20 条逐行刷屏改按顶层目录聚合(坑 reconcile-undeclared-noise,2026-09-15 wp EHS 会话
|
|
1674
|
+
// 实证:58 条③类多为 worktree baseline 拷入的环境文件——agent 需要的是形状不是清单):
|
|
1675
|
+
// 目录计数降序 + 每目录至多 2 个样例;完整清单随后由 writeReconcileRunResult 落盘
|
|
1676
|
+
// reconcile-result.json(gate 输出 📄 行含具体路径),suspectTask 归因以落盘为准。
|
|
1677
|
+
const byDir = new Map()
|
|
1678
|
+
for (const u of r.undeclared) {
|
|
1679
|
+
const p = String(u.path || '')
|
|
1680
|
+
const slash = p.indexOf('/')
|
|
1681
|
+
const dir = slash > 0 ? p.slice(0, slash + 1) : '(仓根)'
|
|
1682
|
+
const rec = byDir.get(dir) || { count: 0, samples: [] }
|
|
1683
|
+
rec.count++
|
|
1684
|
+
if (rec.samples.length < 2) rec.samples.push(p)
|
|
1685
|
+
byDir.set(dir, rec)
|
|
1686
|
+
}
|
|
1687
|
+
const dirs = [...byDir.entries()].sort((a, b) => b[1].count - a[1].count)
|
|
1688
|
+
for (const [dir, rec] of dirs.slice(0, 8)) {
|
|
1689
|
+
console.warn(` - ${dir} ×${rec.count}(样例:${rec.samples.join('、')})`)
|
|
1690
|
+
}
|
|
1691
|
+
if (dirs.length > 8) console.warn(` …另有 ${dirs.length - 8} 个目录`)
|
|
1692
|
+
console.warn(` 共 ${r.undeclared.length} 条,>20 触发按目录聚合;完整逐条清单(含疑似 task 归因)见对账落盘 reconcile-result.json(下方 📄 行)。`)
|
|
1693
|
+
}
|
|
1694
|
+
console.warn(` evidence: matched=${envelope.evidence.matched_count} missing=${envelope.evidence.missing_declared_count} undeclared=${envelope.evidence.undeclared_count}`)
|
|
1661
1695
|
for (const fix of envelope.supportedFixes) console.warn(` 提示:${fix}`)
|
|
1662
1696
|
console.warn(` 对账口径 actual=${(r.sources || []).join(' + ') || 'n/a'}(③类误报先核对 sources 与并行会话剔除提示)。`)
|
|
1663
1697
|
return false
|
package/src/run/prompt.js
CHANGED
|
@@ -1042,7 +1042,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
1042
1042
|
if (['brainstorm', 'plan', 'execute'].includes(stageName) && promptText.includes('{REVIEW_TIER}')) {
|
|
1043
1043
|
try {
|
|
1044
1044
|
const { classifyReviewTier } = await import('../review-tier.js')
|
|
1045
|
-
const { generateStageReviewRunId, renderReviewJsonContract, stageReviewMarkerPath, readReviewChannelPriority } = await import('../stage-review.js')
|
|
1045
|
+
const { generateStageReviewRunId, renderReviewJsonContract, stageReviewMarkerPath, readReviewChannelPriority, getLatestStageReviewRunId, collectSameStagePriorReview, renderPriorRoundFindingsMd } = await import('../stage-review.js')
|
|
1046
1046
|
const tierSpecBase = resolvePromptSpecBase(platformOpts, cwd)
|
|
1047
1047
|
const tierChangeDir = changeName ? join(tierSpecBase, 'changes', changeName) : null
|
|
1048
1048
|
const designPath = tierChangeDir ? join(tierChangeDir, 'design.md') : null
|
|
@@ -1082,11 +1082,52 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
1082
1082
|
// 头部渲染「审查执行通道」段(按用户配置序,见 local.yaml review_dispatch.channel_priority)。
|
|
1083
1083
|
const reviewChannelPriority = readReviewChannelPriority(cwd)
|
|
1084
1084
|
const reviewContractMd = renderReviewJsonContract({ stage: stageName, changeDir: tierChangeDir, reviewRunId, tier: tier.tier, channelPriority: reviewChannelPriority })
|
|
1085
|
+
// C2 前阶段实证清单机械附带(坑 review-subagent-redundant-verify,2026-09-15 wp EHS 实证:
|
|
1086
|
+
// plan 审查把 brainstorm 已两轮实证的三仓源码全量重验——94 分钟不收敛被用户三催。此前只有
|
|
1087
|
+
// prompt 散文说「前序实证勿重验」,子代理并不知道前序具体审过了什么;现把前序 stage review
|
|
1088
|
+
// 的 checklist pass 项机械注入派发 prompt(封顶 15 条防灌爆),审查子代理据此跳过已实证面。
|
|
1089
|
+
let priorFactsMd = ''
|
|
1090
|
+
if (tier.tier === 'independent') {
|
|
1091
|
+
try {
|
|
1092
|
+
const priorStages = stageName === 'plan' ? ['brainstorm'] : stageName === 'execute' ? ['brainstorm', 'plan'] : []
|
|
1093
|
+
const factLines = []
|
|
1094
|
+
for (const ps of priorStages) {
|
|
1095
|
+
let priorRunId = null
|
|
1096
|
+
try { priorRunId = getLatestStageReviewRunId(tierRuntimeRoot, ps, changeName) } catch {}
|
|
1097
|
+
if (!priorRunId) continue
|
|
1098
|
+
const priorReviewPath = join(tierRuntimeRoot, 'stage-reviews', `${ps}-${priorRunId}`, 'review.json')
|
|
1099
|
+
if (!existsSync(priorReviewPath)) continue
|
|
1100
|
+
try {
|
|
1101
|
+
const priorReview = JSON.parse(readFileSync(priorReviewPath, 'utf8'))
|
|
1102
|
+
for (const c of (Array.isArray(priorReview.checklist) ? priorReview.checklist : [])) {
|
|
1103
|
+
if (c && c.result === 'pass' && c.item) factLines.push(`- [${ps}] ${String(c.item).replace(/\s+/g, ' ').slice(0, 100)}`)
|
|
1104
|
+
}
|
|
1105
|
+
} catch {}
|
|
1106
|
+
}
|
|
1107
|
+
if (factLines.length > 0) {
|
|
1108
|
+
const shown = factLines.slice(0, 15)
|
|
1109
|
+
priorFactsMd = `\n**前序阶段独立审查已实证(pass)结论——直接引用勿重验**(展示 ${shown.length}/${factLines.length} 条;EHS 复盘实证:重验已实证面 = 94 分钟不收敛的同款浪费;本阶段只审自己特有面,结论存疑时才定向补证):\n${shown.join('\n')}\n`
|
|
1110
|
+
}
|
|
1111
|
+
} catch {}
|
|
1112
|
+
}
|
|
1113
|
+
// 同阶段上一轮复审基线(ql-20260916-021,Superpowers scoped re-review 采纳①):上一轮
|
|
1114
|
+
// FAIL 的 findings 与已实证 pass 面机械回灌给复审子代理——复审以增量为主(核验修复 +
|
|
1115
|
+
// 本次改版新增面),防全量重读与重复报告已修问题(独立复审子代理无对话历史,不知上一轮
|
|
1116
|
+
// 审过什么/报过什么)。骨架轮/他变更轮由采集端过滤;tier=self 不注入(当前 agent 自带
|
|
1117
|
+
// 上一轮上下文)。
|
|
1118
|
+
try {
|
|
1119
|
+
const priorRound = collectSameStagePriorReview(tierRuntimeRoot, stageName, changeName)
|
|
1120
|
+
if (priorRound) {
|
|
1121
|
+
const blockMd = renderPriorRoundFindingsMd(priorRound)
|
|
1122
|
+
priorFactsMd = priorFactsMd ? priorFactsMd + blockMd : blockMd
|
|
1123
|
+
}
|
|
1124
|
+
} catch {}
|
|
1085
1125
|
promptText = promptText
|
|
1086
1126
|
.split('{REVIEW_TIER}').join(tier.tier)
|
|
1087
1127
|
.split('{REVIEW_TIER_REASON}').join(tier.reason)
|
|
1088
1128
|
.split('{STAGE_REVIEW_RUN_ID}').join(reviewRunId)
|
|
1089
1129
|
.split('{REVIEW_JSON_CONTRACT}').join(reviewContractMd)
|
|
1130
|
+
.split('{PRIOR_REVIEW_FACTS}').join(priorFactsMd)
|
|
1090
1131
|
} catch (e) {
|
|
1091
1132
|
// 降级 self,避免 prompt 残留占位符
|
|
1092
1133
|
promptText = promptText
|
|
@@ -1094,6 +1135,7 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
1094
1135
|
.split('{REVIEW_TIER_REASON}').join('分级异常降级 self: ' + e.message)
|
|
1095
1136
|
.split('{STAGE_REVIEW_RUN_ID}').join('review-unknown')
|
|
1096
1137
|
.split('{REVIEW_JSON_CONTRACT}').join('(review 契约注入失败,按 schemaVersion=1 + reviewType + verdicts∈pass/fail/cannot_verify + reviewedFiles + docHash=主文档 sha256 产出)')
|
|
1138
|
+
.split('{PRIOR_REVIEW_FACTS}').join('')
|
|
1097
1139
|
}
|
|
1098
1140
|
}
|
|
1099
1141
|
|
|
@@ -1270,6 +1312,42 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
1270
1312
|
promptText = promptText.split('{SCOPE_AUDIT_TABLE}').join(scopeTable)
|
|
1271
1313
|
}
|
|
1272
1314
|
|
|
1315
|
+
// {HANDOVER_SUMMARY}(2026-09-17-pass-cap-semantics task-04,FR-06 注入态):archive「确认
|
|
1316
|
+
// 归档」步的移交项清单注入——读 changes/<name>/verify-facts.json 的 facts.handover(task-01
|
|
1317
|
+
// 产出 { count, items[] },items 行含 type/item/condition/severity),blocking 置顶 + severity
|
|
1318
|
+
// 标注 + 封顶渲染(maxRows 8,对齐 {SCOPE_AUDIT_TABLE} 60/量级收窄——移交项是裁决面不是
|
|
1319
|
+
// 对账面),表尾指路 verify-facts.json 看全量。fail-soft:handover 段缺失/零条目/facts.json
|
|
1320
|
+
// 缺失 → 空串(正常态零输出);读取异常降级单行指引——注入永不阻断归档 prompt 输出
|
|
1321
|
+
//(与门阻断语义分离,对齐 {SCOPE_AUDIT_TABLE} 注入先例)。
|
|
1322
|
+
if (stageName === 'archive' && promptText.includes('{HANDOVER_SUMMARY}')) {
|
|
1323
|
+
let handoverBlock = ''
|
|
1324
|
+
try {
|
|
1325
|
+
const hsSpecBase = resolvePromptSpecBase(platformOpts, cwd)
|
|
1326
|
+
const hsFactsPath = changeName ? join(hsSpecBase, 'changes', changeName, 'verify-facts.json') : null
|
|
1327
|
+
if (hsFactsPath && existsSync(hsFactsPath)) {
|
|
1328
|
+
const hsFacts = JSON.parse(readFileSync(hsFactsPath, 'utf8'))
|
|
1329
|
+
const hsItems = (hsFacts && hsFacts.handover && Array.isArray(hsFacts.handover.items)) ? hsFacts.handover.items : []
|
|
1330
|
+
if (hsItems.length > 0) {
|
|
1331
|
+
const HS_MAX_ROWS = 8
|
|
1332
|
+
const sevOf = (it) => (it && it.severity === 'blocking') ? 'blocking' : 'advisory'
|
|
1333
|
+
const sorted = [...hsItems].sort((a, b) => (sevOf(a) === 'blocking' ? 0 : 1) - (sevOf(b) === 'blocking' ? 0 : 1))
|
|
1334
|
+
const rows = sorted.slice(0, HS_MAX_ROWS).map((it) => {
|
|
1335
|
+
const cond = it && it.condition ? ` | 条件: ${it.condition}` : ''
|
|
1336
|
+
return `- [${sevOf(it)}] ${(it && it.type) || 'unknown'} | ${(it && it.item) || ''}${cond}`
|
|
1337
|
+
})
|
|
1338
|
+
handoverBlock = [
|
|
1339
|
+
`【移交项清单(CLI 机械注入,共 ${hsItems.length} 条,blocking 置顶)】`,
|
|
1340
|
+
...rows,
|
|
1341
|
+
...(sorted.length > HS_MAX_ROWS ? [`……(其余 ${sorted.length - HS_MAX_ROWS} 条略——全量见 verify-facts.json)`] : []),
|
|
1342
|
+
].join(String.fromCharCode(10))
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
} catch (e) {
|
|
1346
|
+
handoverBlock = '(移交项清单注入失败:' + (e && e.message ? e.message : e) + '——回退读 changes/<变更名>/verify-facts.json 的 handover 段核对后再确认归档,注入失败不阻断归档)'
|
|
1347
|
+
}
|
|
1348
|
+
promptText = promptText.split('{HANDOVER_SUMMARY}').join(handoverBlock)
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1273
1351
|
// 注入模块上下文(brainstorm/plan/execute 阶段全步 + quick 首步「理解任务」——刀①:quick
|
|
1274
1352
|
// step1 原让 agent cat module-map 再挑模块卡读,改为按任务描述匹配后注入,基于 Module Context Index)
|
|
1275
1353
|
const quickFirstStep = stageName === 'quick' && step && step.name === '理解任务'
|
package/src/run/shared.js
CHANGED
|
@@ -124,6 +124,12 @@ export function resolvePromptIncludes(text) {
|
|
|
124
124
|
* home 拒绝守卫:home 下的 .sillyspec 恒不命中——历史污染源(smoke 测试在 home 下临时目录
|
|
125
125
|
* 跑 CLI,_ensureDB 读路径即建库,~/.sillyspec 长出一整套平行进度库后,任何 home 子目录
|
|
126
126
|
* 跑命令都会向上撞它,污染自我延续)。撞 home 即停,回退 cwd/.sillyspec(子目录自建)。
|
|
127
|
+
* tmpdir 硬边界(坑 temp-root-stray-sillyspec,2026-09-16-friction5-hardening verify 移交项①
|
|
128
|
+
* 实证):某次 cwd=%TEMP% 直系的调用在 Temp 根遗留游离 .sillyspec 后,所有 temp 夹具的祖先
|
|
129
|
+
* 链都被它劫持(agent-automation-batch4『add frontend 建档』/feedback-batch2 两测试假败的真
|
|
130
|
+
* 根因——specBase 解析到 Temp\.sillyspec,夹具内断言读自身路径 ENOENT)。temp 下合法的
|
|
131
|
+
* .sillyspec 只会在夹具自身内部:起点在 tmpdir 子树内时,tmpdir 层及其上永不检查、走到即停
|
|
132
|
+
* (比 home 守卫更硬:home 是「跳过一层继续走」,tmp 是「整段封顶」——tmpdir 之上无合法项目根)。
|
|
127
133
|
* @param {string} cwd
|
|
128
134
|
* @param {object} [opts]
|
|
129
135
|
* @param {string} [opts.specDir] - 用户指定的 specDir(通过 --spec-dir 或 --spec-root)
|
|
@@ -132,11 +138,15 @@ export function resolvePromptIncludes(text) {
|
|
|
132
138
|
export function resolveSpecDir(cwd, opts = {}) {
|
|
133
139
|
if (opts.specDir) return resolve(opts.specDir)
|
|
134
140
|
const home = os.homedir()
|
|
141
|
+
const tmp = os.tmpdir()
|
|
135
142
|
const origin = resolve(cwd)
|
|
136
143
|
const originBelowHome = origin.startsWith(home + sep) // 严格子目录,不含 home 自身
|
|
144
|
+
const originInTmp = origin === tmp || origin.startsWith(tmp + sep) // 含 tmpdir 自身
|
|
137
145
|
let dir = origin
|
|
138
146
|
let passedHome = false
|
|
139
147
|
while (true) {
|
|
148
|
+
// tmpdir 硬边界:起点在 tmpdir 子树内时,走到 tmpdir 层即封顶(该层及其上是游离库温床,永不命中)
|
|
149
|
+
if (originInTmp && dir === tmp) break
|
|
140
150
|
// home 拒绝守卫:起点在 home 子树内时,home 层不检查 .sillyspec,
|
|
141
151
|
// 防止 home 子目录遍历经过 home 误命中 ~/.sillyspec;
|
|
142
152
|
// passedHome:经过 home 后不再检查(覆盖遍历到真实 home 的场景)。
|