sillyspec 3.28.4 → 3.28.6
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/AGENTS.md +1 -0
- package/package.json +1 -1
- package/results-review-tier.md +18 -0
- package/sa-rename-probe/src/new.js +4 -0
- package/src/agent-session-log.js +4 -3
- package/src/archive-delta.js +2 -2
- package/src/change-list.js +11 -1
- package/src/commit-suggest.js +52 -1
- package/src/config-cat.js +6 -1
- package/src/config-schema.js +13 -0
- package/src/decision-distill.js +53 -7
- package/src/decisions-io.js +96 -0
- package/src/design-facts.js +3 -0
- package/src/dispatch/probe.js +31 -8
- package/src/docs-check.js +77 -8
- package/src/friction-tally.js +221 -0
- package/src/index.js +430 -18
- package/src/mcp-server.js +144 -0
- package/src/module-impact.js +8 -1
- package/src/modules.js +46 -34
- package/src/progress/change-registry.js +20 -0
- package/src/progress.js +16 -1
- package/src/quicklog.js +60 -6
- package/src/review-dispatch.js +377 -0
- package/src/run/archive-distill.js +48 -0
- package/src/run/command.js +35 -5
- package/src/run/complete-handlers.js +136 -3
- package/src/run/complete.js +170 -2
- package/src/run/gate-snapshot.js +159 -0
- package/src/run/gates.js +166 -69
- package/src/run/prompt.js +54 -9
- package/src/run/quick-audit.js +78 -16
- package/src/run/shared.js +138 -7
- package/src/run/stage.js +31 -2
- package/src/run/verify-quality-scan.js +383 -0
- package/src/scope-audit.js +941 -0
- package/src/sillyhub-mcp/client.js +58 -3
- package/src/stage-review-checklist.js +44 -0
- package/src/stage-review.js +122 -4
- package/src/stages/archive.js +57 -164
- package/src/stages/brainstorm.js +8 -12
- package/src/stages/execute.js +13 -10
- package/src/stages/plan.js +3 -10
- package/src/stages/verify.js +14 -33
- package/src/sync.js +51 -10
- package/src/task-review.js +108 -16
- package/src/taskcard.js +46 -3
- package/src/validate-artifacts.js +163 -0
- package/src/verify-postcheck.js +90 -10
- package/src/verify-probes.js +134 -12
- package/src/worktree-apply.js +119 -14
- package/templates/agents-instruction.md +1 -0
|
@@ -34,6 +34,8 @@ import { printQuickAuditReview, runQuickTestLintGate, printQuickTestLintGate } f
|
|
|
34
34
|
import { validateQuickResult, allocateQuicklogEntry, appendQuicklogEntryWithId, findQuicklogEntry, completeQuicklogEntry, extractTitleFromResult, parseFileNotes, getQuickFileNotes } from '../quicklog.js'
|
|
35
35
|
import { getRule } from '../stage-contract-spec.js'
|
|
36
36
|
import { archiveDestDirName } from '../stage-contract.js'
|
|
37
|
+
import { collectNumstatByPath } from '../scope-audit.js'
|
|
38
|
+
import { recordFrictionEvent, consumeFrictionHint } from '../friction-tally.js'
|
|
37
39
|
|
|
38
40
|
/**
|
|
39
41
|
* 清洗项目名:只保留 ASCII 字母/数字/横线/下划线/点,过滤中文和特殊字符。
|
|
@@ -169,6 +171,13 @@ export function pruneArchivedChangeRuntime(runtimeRoot, changeName) {
|
|
|
169
171
|
if (existsSync(pathspec)) gone(pathspec)
|
|
170
172
|
} catch {}
|
|
171
173
|
|
|
174
|
+
// friction tally(friction-signal-hint FR-05):摩擦计数随变更终态一并回收——真实变更的
|
|
175
|
+
// friction-tally-<change>.json 落 runtimeRoot,变更归档/删除后无读者,残留即孤儿文件。
|
|
176
|
+
try {
|
|
177
|
+
const fp = join(runtimeRoot, `friction-tally-${changeName}.json`)
|
|
178
|
+
if (existsSync(fp)) gone(fp)
|
|
179
|
+
} catch {}
|
|
180
|
+
|
|
172
181
|
try {
|
|
173
182
|
const execDir = join(runtimeRoot, 'execute-runs')
|
|
174
183
|
if (existsSync(execDir)) {
|
|
@@ -1055,6 +1064,9 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
|
|
|
1055
1064
|
steps[currentIdx].status = 'pending'
|
|
1056
1065
|
steps[currentIdx].completedAt = null
|
|
1057
1066
|
if (outputText) steps[currentIdx].output = null
|
|
1067
|
+
// 摩擦计数(friction-signal-hint task-04):quick 审计被拦也是摩擦信号——exit 前记
|
|
1068
|
+
// 一笔(changeName=quick 会话 id,tally 落 session 目录,成功收尾 consume 后随目录清理)。
|
|
1069
|
+
recordFrictionEvent({ cwd, changeName, platformOpts, type: 'gate_rollback', detail: 'quick-audit' })
|
|
1058
1070
|
process.exit(1)
|
|
1059
1071
|
}
|
|
1060
1072
|
// P0-2(2026-09-02 跨 agent 工单):触及 src/test 的 quick --done 内置 test+lint
|
|
@@ -1075,6 +1087,9 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
|
|
|
1075
1087
|
steps[currentIdx].status = 'pending'
|
|
1076
1088
|
steps[currentIdx].completedAt = null
|
|
1077
1089
|
if (outputText) steps[currentIdx].output = null
|
|
1090
|
+
// 摩擦计数(friction-signal-hint task-04):test/lint 实测门禁拦下同记摩擦——exit 前
|
|
1091
|
+
// 落 session tally,与 quick-audit 同款静默降级。
|
|
1092
|
+
recordFrictionEvent({ cwd, changeName, platformOpts, type: 'gate_rollback', detail: 'quick-test-lint' })
|
|
1078
1093
|
process.exit(1)
|
|
1079
1094
|
}
|
|
1080
1095
|
progress.lastQuickReview = review
|
|
@@ -1203,6 +1218,14 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
|
|
|
1203
1218
|
? review.attributedFiles
|
|
1204
1219
|
: (Array.isArray(review?.changedFiles) ? review.changedFiles : [])
|
|
1205
1220
|
const realFiles = auditFiles.filter(f => !isQuicklogFileLineNoise(f, linkedChanges))
|
|
1221
|
+
// 软归属(2026-09-10 用户反馈:--file-notes 括注只落声明文件,同模块测试文件漏声明时文件行
|
|
1222
|
+
// 不自动补齐,连续多批手工核对):auditQuickCompletion 判定的「窗口内未声明同模块测试文件」
|
|
1223
|
+
// → 补入文件行 bullet 带软归属括注;审计面单列 🔍 行(可追溯、错认可手工剔除),⚠️ 不进
|
|
1224
|
+
// attributedFiles——硬归属口径「声明即归属」不变(2026-08-18 误归属修复)。
|
|
1225
|
+
const softFiles = (Array.isArray(review?.softTestFiles) ? review.softTestFiles : [])
|
|
1226
|
+
.map(f => String(f).replace(/\\/g, '/')).filter(f => !isQuickMetadata(f, linkedChanges))
|
|
1227
|
+
// task-06(FR-04):文件行/审计行并上 numstat 行数——attachQuickLineCounts 见文件尾(声明提升)
|
|
1228
|
+
const { annotatedRealFiles, annotatedSoftFiles, fmtLineCounts } = attachQuickLineCounts({ cwd, realFiles, softFiles, review, auditFiles, qlId })
|
|
1206
1229
|
// D-8 落盘(2026-08-18 修):advisory 欠账信号从「纯打印」升级为「随条目落盘」——修复
|
|
1207
1230
|
// 「欠账已记录(QUICKLOG reasons)」的不实承诺(交叉审查实证 reasons 纯 stdout,事后不可审计)。
|
|
1208
1231
|
// 两周实测(2026-08-31 裁决,doc-consistency-debt §七)需要分母:信号触发次数必须可追溯。
|
|
@@ -1214,6 +1237,10 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
|
|
|
1214
1237
|
}
|
|
1215
1238
|
if (review?.docsCheckHint && review.docsCheckHint.invalid > 0) {
|
|
1216
1239
|
auditNotes.push(`📎 文档引用失效:${review.docsCheckHint.invalid}/${review.docsCheckHint.total} 处 file:line 失效(sillyspec docs check 可复现)`)
|
|
1240
|
+
// 逐条指名(2026-09-11 同 quick-audit 侧:门禁输出直接带文件:行号,封顶 5 保持注记紧凑)
|
|
1241
|
+
for (const i of (review.docsCheckHint.invalidRefs || []).slice(0, 5)) {
|
|
1242
|
+
auditNotes.push(` ❌ [${i.doc}:${i.docLine}] ${i.ref} → ${i.reason}`)
|
|
1243
|
+
}
|
|
1217
1244
|
// 行号漂移自动重锚(2026-09-08 用户反馈②:活文档 file:line 硬编码随任何插入失效,
|
|
1218
1245
|
// 每回手跑 docs check --fix 是机械活):autoReanchorDocRefs = --fix 主链路编程化封装
|
|
1219
1246
|
// (fixable 唯一/优选命中才改 + 定点替换 + 同口径回执),只作用于本次改动的文档。
|
|
@@ -1236,19 +1263,74 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
|
|
|
1236
1263
|
}
|
|
1237
1264
|
// 归属切分注(2026-08-18 误归属修复):窗口内未声明脏文件不进「文件:」行,但必须落盘可追溯
|
|
1238
1265
|
// (多 agent 并发仓他者窗口改动 / 本会话漏声明均可能),防真实改动被静默挤走。
|
|
1266
|
+
// 软归属拆分(2026-09-10):softFiles 中的同模块测试文件已补入文件行,不再占 ⚖️「未计入」
|
|
1267
|
+
// 文案(否则审计行与文件行自相矛盾),单列 🔍 行交代软归属依据与剔除指引。
|
|
1239
1268
|
if (Array.isArray(review?.undeclaredFiles) && review.undeclaredFiles.length > 0) {
|
|
1240
|
-
const
|
|
1269
|
+
const softSet = new Set(softFiles)
|
|
1270
|
+
const undeclared = review.undeclaredFiles.filter(f => !isQuickMetadata(f, linkedChanges) && !softSet.has(String(f).replace(/\\/g, '/')))
|
|
1241
1271
|
if (undeclared.length > 0) {
|
|
1242
|
-
auditNotes.push(`⚖️ 归属切分:${undeclared.length} 个窗口内未声明脏文件未计入文件行(并行会话改动或本会话漏声明):${undeclared.join(', ')}`)
|
|
1272
|
+
auditNotes.push(`⚖️ 归属切分:${undeclared.length} 个窗口内未声明脏文件未计入文件行(并行会话改动或本会话漏声明):${undeclared.map(f => `${f}${fmtLineCounts(f)}`).join(', ')}`)
|
|
1273
|
+
}
|
|
1274
|
+
if (softFiles.length > 0) {
|
|
1275
|
+
auditNotes.push(`🔍 软归属:${softFiles.length} 个窗口内未声明同模块测试文件已补入文件行(若属并行会话改动请手工剔除):${annotatedSoftFiles.join(', ')}`)
|
|
1276
|
+
console.log(`🔍 软归属:${softFiles.length} 个窗口内未声明同模块测试文件已按软归属补入文件行(若属并行会话改动请手工剔除):${annotatedSoftFiles.join(', ')}`)
|
|
1243
1277
|
}
|
|
1244
1278
|
}
|
|
1245
1279
|
await completeQuicklogEntry(specBase, gitUser, qlId, {
|
|
1246
1280
|
resultText: outputText || '',
|
|
1247
1281
|
linkedChanges,
|
|
1248
|
-
changedFiles:
|
|
1282
|
+
changedFiles: annotatedRealFiles,
|
|
1249
1283
|
auditNotes,
|
|
1284
|
+
softFiles,
|
|
1250
1285
|
})
|
|
1251
1286
|
console.log(`📝 QUICKLOG 条目 ${qlId} 已标记完成`)
|
|
1287
|
+
// 范围快照/patch 审计级落盘(quick-359a48f1):quicklog/patches/<qlId>.json+.patch——按
|
|
1288
|
+
// ql-ID 对齐 QUICKLOG 条目(平台按条目抓取),json 冗余 sessionId 供 guard 清理后记录态
|
|
1289
|
+
// 反查(computeQuickAudit → findQuickPatchRecord 同链)。patch 为 --done 时点全量冻结
|
|
1290
|
+
// (HEAD 未提交窗口,untracked 自拼 hunk)。fail-soft:失败只提示,不阻断收尾。
|
|
1291
|
+
try {
|
|
1292
|
+
const { computeChangeScopeAudit, collectNumstatByPath, buildFrozenPatch } = await import('../scope-audit.js')
|
|
1293
|
+
// changeName == sessionId(handleQuickStageCompletion 作用域内,§4.6 注释同款约定)
|
|
1294
|
+
const snapResult = await computeChangeScopeAudit({ cwd, specBase, changeName, platformOpts, collectPatch: true })
|
|
1295
|
+
let quickRows = snapResult && snapResult.mode === 'quick' && Array.isArray(snapResult.rows)
|
|
1296
|
+
? snapResult.rows.filter(r => r && r.attribution !== 'undeclared')
|
|
1297
|
+
: null
|
|
1298
|
+
let frozenPatch = snapResult ? snapResult.frozenPatch : undefined
|
|
1299
|
+
// 窗口空兜底(baseline 会话常态):--files 声明文件是会话启动时预存脏(audit baseline
|
|
1300
|
+
// 剔除后窗口空,quickRows 空),但声明即归属——改用声明文件集构造 rows + patch(行数按
|
|
1301
|
+
// HEAD 未提交窗口采集,同口径)。
|
|
1302
|
+
if ((!quickRows || quickRows.length === 0) && Array.isArray(guard?.allowedFiles) && guard.allowedFiles.length > 0) {
|
|
1303
|
+
const sessionRoot = dirname(specBase)
|
|
1304
|
+
const declaredPosix = guard.allowedFiles.map(f => String(f).replace(/\\/g, '/')).filter(Boolean)
|
|
1305
|
+
const stats = collectNumstatByPath(sessionRoot, declaredPosix, { baseRef: 'HEAD' })
|
|
1306
|
+
quickRows = declaredPosix.filter(f => stats.has(f) || existsSync(join(sessionRoot, f)))
|
|
1307
|
+
.map(f => {
|
|
1308
|
+
const st = stats.get(f) || { additions: null, deletions: null, kind: existsSync(join(sessionRoot, f)) ? 'modified' : 'deleted' }
|
|
1309
|
+
return { path: f, declared: true, additions: st.additions, deletions: st.deletions, kind: st.kind, attribution: 'declared' }
|
|
1310
|
+
})
|
|
1311
|
+
frozenPatch = buildFrozenPatch(sessionRoot, quickRows.map(r => r.path), { baseRef: 'HEAD' })
|
|
1312
|
+
}
|
|
1313
|
+
if (quickRows && quickRows.length > 0) {
|
|
1314
|
+
const patchesDir = join(specBase, 'quicklog', 'patches')
|
|
1315
|
+
mkdirSync(patchesDir, { recursive: true })
|
|
1316
|
+
const { ...snapRest } = snapResult || {}
|
|
1317
|
+
delete snapRest.frozenPatch
|
|
1318
|
+
writeFileSync(join(patchesDir, `${qlId}.json`), JSON.stringify({
|
|
1319
|
+
...snapRest,
|
|
1320
|
+
rows: quickRows,
|
|
1321
|
+
totals: { files: quickRows.length, additions: quickRows.reduce((n, r) => n + (Number.isFinite(r.additions) ? r.additions : 0), 0), deletions: quickRows.reduce((n, r) => n + (Number.isFinite(r.deletions) ? r.deletions : 0), 0) },
|
|
1322
|
+
qlId,
|
|
1323
|
+
sessionId: changeName,
|
|
1324
|
+
savedAt: new Date().toISOString(),
|
|
1325
|
+
}, null, 2) + '\n')
|
|
1326
|
+
if (typeof frozenPatch === 'string' && frozenPatch) {
|
|
1327
|
+
writeFileSync(join(patchesDir, `${qlId}.patch`), frozenPatch.endsWith('\n') ? frozenPatch : frozenPatch + '\n')
|
|
1328
|
+
}
|
|
1329
|
+
console.log(`📦 范围快照已落 quicklog/patches/${qlId}.json${typeof frozenPatch === 'string' && frozenPatch ? ' + .patch(--done 时点冻结)' : ''}`)
|
|
1330
|
+
}
|
|
1331
|
+
} catch (e) {
|
|
1332
|
+
console.warn(` ⚠️ quick 范围快照落盘失败(不阻断收尾):${e && e.message ? String(e.message).split('\n')[0] : e}`)
|
|
1333
|
+
}
|
|
1252
1334
|
// 刷新 DB title:从 step3「需求:」提取(agent 可改 title 的途径),覆盖启动时的兜底快照。
|
|
1253
1335
|
// 与 QUICKLOG 标题刷新(flipEntryInContent 内 extractTitleFromResult)同源,保持 DB↔QUICKLOG 一致。
|
|
1254
1336
|
try {
|
|
@@ -1274,6 +1356,14 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
|
|
|
1274
1356
|
console.warn(`⚠️ 关联变更轻量归档失败(不阻断 quick 完成): ${e.message}`)
|
|
1275
1357
|
}
|
|
1276
1358
|
|
|
1359
|
+
// 摩擦信号消费(friction-signal-hint task-04):必须在 session 目录清理**前**——清理后
|
|
1360
|
+
// tally 随目录删除,晚于此就读不到。QUICKLOG 完成打印之后 consume,提示里的 postmortem
|
|
1361
|
+
// 建议有落点可循;全零/读失败零输出,摩擦提示失败不影响收尾。
|
|
1362
|
+
try {
|
|
1363
|
+
const fr = consumeFrictionHint({ cwd, changeName, platformOpts })
|
|
1364
|
+
if (fr && fr.hint) console.log(`\n${fr.hint}`)
|
|
1365
|
+
} catch { /* 摩擦提示失败不影响收尾 */ }
|
|
1366
|
+
|
|
1277
1367
|
// 清理 session 目录(rmSync/unlinkSync 容忍不存在)。路径与写入对齐(Q4 resolveQuickSessionsDir)。
|
|
1278
1368
|
try {
|
|
1279
1369
|
if (changeName) {
|
|
@@ -1828,3 +1918,46 @@ export async function handleScanStageCompleted({ stageName, currentIdx, cwd, pro
|
|
|
1828
1918
|
return null
|
|
1829
1919
|
}
|
|
1830
1920
|
|
|
1921
|
+
// ── task-06(2026-09-10-change-scope-audit,FR-04):quick 文件行/审计行 numstat 行数注入 ──
|
|
1922
|
+
// collectNumstatByPath 与 scope-audit 命令同源采集(D-003 单一真相;R-03 单次 git 调用非逐文件)。
|
|
1923
|
+
// 基点 'HEAD' = 未提交工作区窗口(D-004:quick 无 merge-base 锚,对未提交窗口采集语义自洽;
|
|
1924
|
+
// untracked 新文件 wc-l 记全 + 行、binary 显 BIN)。纯展示列(advisory,D-006):采集失败/行数
|
|
1925
|
+
// 不可得 → 括注省略不出伪数据;不改 auditQuickCompletion 判定、runQuickTestLintGate 与任何门禁。
|
|
1926
|
+
// 放文件尾(函数声明提升)而不内联进 handleQuickStageCompletion:docs/platform-interface-map
|
|
1927
|
+
// 行号锚在上方(complete-handlers.js:1695 handleScanStageCompleted),内联大块会把 doc-ref 锚
|
|
1928
|
+
// 推出关键词窗口。
|
|
1929
|
+
//
|
|
1930
|
+
// 消费分工(调用方 quick 收尾):
|
|
1931
|
+
// - annotatedRealFiles → completeQuicklogEntry changedFiles(QUICKLOG「文件:」行/bullet 带括注;
|
|
1932
|
+
// fileNotes 模式下 realFiles 本就不渲染,括注只出现在回退渲染面)
|
|
1933
|
+
// - annotatedSoftFiles → 🔍 软归属审计行 + console;softFiles 传参保持**裸路径**——
|
|
1934
|
+
// flipEntryInContent 用它与 fileNotes.path 精确等值去重(带括注会不等值 → 同文件双 bullet)
|
|
1935
|
+
// - fmtLineCounts → ⚖️ 归属切分审计行逐文件括注(降级返回空串 → 原样不带)
|
|
1936
|
+
// - 已提交降级(D-004):窗口空(改动已 commit/会话未产生改动)且 QUICKLOG 条目已存在 →
|
|
1937
|
+
// 记录态提示读 QUICKLOG 条目文件行,不以空表冒充实时(与 scope-audit quick 模式 note 同口径)
|
|
1938
|
+
function attachQuickLineCounts({ cwd, realFiles, softFiles, review, auditFiles, qlId }) {
|
|
1939
|
+
const posix = (f) => String(f).replace(/\\/g, '/')
|
|
1940
|
+
const lineCountFiles = [...new Set([...(realFiles || []).map(posix), ...(softFiles || [])])]
|
|
1941
|
+
let numstatByPath = new Map()
|
|
1942
|
+
if (lineCountFiles.length > 0) {
|
|
1943
|
+
try {
|
|
1944
|
+
numstatByPath = collectNumstatByPath(cwd, lineCountFiles, { baseRef: 'HEAD' })
|
|
1945
|
+
} catch { /* 采集异常 → 行数列整体降级省略(fail-soft) */ }
|
|
1946
|
+
}
|
|
1947
|
+
const fmtLineCounts = (f) => {
|
|
1948
|
+
const st = numstatByPath.get(posix(f))
|
|
1949
|
+
if (!st) return ''
|
|
1950
|
+
if (st.kind === 'binary') return '(BIN)'
|
|
1951
|
+
if (!Number.isFinite(st.additions) || !Number.isFinite(st.deletions)) return ''
|
|
1952
|
+
return `(+${st.additions}/-${st.deletions})`
|
|
1953
|
+
}
|
|
1954
|
+
const annotatedRealFiles = (realFiles || []).map(f => `${f}${fmtLineCounts(f)}`)
|
|
1955
|
+
const annotatedSoftFiles = (softFiles || []).map(f => `${f}${fmtLineCounts(f)}`)
|
|
1956
|
+
if (lineCountFiles.length > 0) {
|
|
1957
|
+
console.log(`📊 变更行数(vs HEAD 未提交窗口):${[...annotatedRealFiles, ...annotatedSoftFiles].join(', ')}`)
|
|
1958
|
+
} else if (review && Array.isArray(auditFiles) && auditFiles.length === 0) {
|
|
1959
|
+
console.log(`ℹ️ 本会话无未提交改动(窗口已提交或会话未产生改动)——实时行数不可采,文件行请读 QUICKLOG 条目 ${qlId}(记录态)`)
|
|
1960
|
+
}
|
|
1961
|
+
return { annotatedRealFiles, annotatedSoftFiles, fmtLineCounts }
|
|
1962
|
+
}
|
|
1963
|
+
|
package/src/run/complete.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { join } from 'node:path'
|
|
20
20
|
import { existsSync, readFileSync, mkdirSync, writeFileSync, appendFileSync } from 'node:fs'
|
|
21
21
|
import { writeAtomicSync } from '../fs-atomic.js'
|
|
22
|
+
import { gitQuiet } from '../git-helper.js'
|
|
22
23
|
import { withFileLock } from '../quicklog.js'
|
|
23
24
|
import { triggerSync, WAIT_MARKER_RE, getStageSteps, formatWaitOptions, resolveRuntimeRoot, getOrCreateMultiRepoContext, resolveChangeDir } from './shared.js'
|
|
24
25
|
|
|
@@ -48,7 +49,6 @@ import { formatExecuteSummary } from '../worktree-apply.js'
|
|
|
48
49
|
import { validateDecisionModuleRefs } from '../design-facts.js'
|
|
49
50
|
import { isEndToEndTaskText } from '../change-risk-profile.js'
|
|
50
51
|
import { deriveTitleFromLinkedChange } from '../quicklog.js'
|
|
51
|
-
import { gitQuiet } from '../git-helper.js'
|
|
52
52
|
|
|
53
53
|
// validateMetadata / readDesignScale / validateFileLocations 已迁至 ./gates.js(completeStageGates
|
|
54
54
|
// 共享收尾管线,消除 noAI 末步 / continueStep 完成分支绕过 gate 的 S1/S2/S3 不对称)。
|
|
@@ -97,6 +97,26 @@ export function resolveWaitingStepWithAnswer(steps, doneAnswer, nowStr) {
|
|
|
97
97
|
return waitIdx
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* 事实性步骤摘要合成(P0-2,noai-ir-roadmap §3):--output 省略时由 CLI 按既有事实合成
|
|
102
|
+
* ——步骤名 + 变更窗口(git porcelain 计数与前几个路径)+ 门禁以 CLI 校验输出为准。
|
|
103
|
+
* 纯事实零编造(无「通过/成功」等判断词——判断在 gate,不在摘要);语义性说明(方案
|
|
104
|
+
* 取舍/用户反馈)不合成,摘要自带手写指引。长度控制在 MAX_OUTPUT(200) 截断线内。
|
|
105
|
+
*/
|
|
106
|
+
export function synthesizeStepOutput({ stageName, stepName, cwd }) {
|
|
107
|
+
let windowNote = ''
|
|
108
|
+
try {
|
|
109
|
+
const porcelain = gitQuiet(cwd, ['status', '--porcelain'])
|
|
110
|
+
if (porcelain !== null) {
|
|
111
|
+
const paths = porcelain.split('\n').filter(Boolean).map((l) => (l.slice(3) || '').trim()).filter(Boolean)
|
|
112
|
+
windowNote = paths.length === 0
|
|
113
|
+
? ';工作区无未提交变更'
|
|
114
|
+
: `;变更窗口 ${paths.length} 文件(${paths.slice(0, 3).map((p) => p.split(/[\\/]/).pop()).join('、')}${paths.length > 3 ? ' 等' : ''})`
|
|
115
|
+
}
|
|
116
|
+
} catch { /* git 不可读 → 窗口段缺省(摘要仍成立) */ }
|
|
117
|
+
return `【CLI 合成】步骤「${stepName}」完成${windowNote};门禁以本次 CLI 校验输出为准(方案取舍等语义说明需 --output 手写)`
|
|
118
|
+
}
|
|
119
|
+
|
|
100
120
|
export async function completeStep(pm, progress, stageName, cwd, outputText, inputText = null, options = {}) {
|
|
101
121
|
const { printNext = true, confirm = false, changeName, platformOpts = {}, nonInteractive = false, isForceBaseline = false, isAllowNew = false, isAllowDelete = false, quickFiles = [] } = options
|
|
102
122
|
// specRoot(平台) > specDriftAnchor(worktree 漂移锚定主仓) > cwd/.sillyspec(本地)——
|
|
@@ -158,6 +178,16 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
158
178
|
currentIdx = _resolvedWaitIdx
|
|
159
179
|
console.log(`⚠️ Step "${steps[_resolvedWaitIdx].name}" 此前处于 waiting,--done --answer 已补回答并拉回待完成。`)
|
|
160
180
|
}
|
|
181
|
+
|
|
182
|
+
// ── P0-2(noai-ir-roadmap §3):--output 省略 → CLI 按事实合成步骤摘要 ──
|
|
183
|
+
// agent 手打摘要是每步命中的幻觉源;事实面(步骤名 + 变更窗口 + 门禁以 CLI 校验为准)
|
|
184
|
+
// CLI 本就持有,纯事实合成零编造(判断词留给 gate)。语义性说明(方案取舍/用户反馈)仍应
|
|
185
|
+
// --output 手写——合成摘要自带该指引。quick 阶段除外(末步四字段硬契约
|
|
186
|
+
// validateQuickResult,合成文本过不了四字段校验;quick 的省略提示由其阶段文案承担)。
|
|
187
|
+
if (!outputText && stageName !== 'quick' && currentIdx !== -1 && steps[currentIdx]) {
|
|
188
|
+
outputText = synthesizeStepOutput({ stageName, stepName: steps[currentIdx].name, cwd })
|
|
189
|
+
console.log(`🤖 --output 未提供——CLI 已按事实合成步骤摘要(gate 校验照常;方案取舍等语义说明下次带 --output 手写)。`)
|
|
190
|
+
}
|
|
161
191
|
// ── waiting 前置守卫(坑 archive-step3-wait-answer-hint-late)──
|
|
162
192
|
// 普通 --done(无 --answer)时若存在 waiting 步骤:currentIdx 会跳过它推进后续步骤
|
|
163
193
|
//(findIndex 只查 pending/in-progress),步骤被静默越过 + --answer 要求要到别处报错才暴露。
|
|
@@ -380,6 +410,16 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
380
410
|
} else if (_cliAction === 'progressConfirm') {
|
|
381
411
|
// 与 stage.js noAI 分支同语义(brainstorm/execute/verify step1 进度确认)
|
|
382
412
|
executeProgressConfirm({ stageName, cwd, stageData, changeName, pm })
|
|
413
|
+
} else if (_cliAction === 'verifyRunQualityScan') {
|
|
414
|
+
// 与 stage.js noAI 分支同语义(P0-1 noai-ir-roadmap §3:verify 测试步实测提前 + 指纹复用;
|
|
415
|
+
// 失败 throw → completeStep 不推进,步骤保持 pending)
|
|
416
|
+
const { executeVerifyQualityScan } = await import('./verify-quality-scan.js')
|
|
417
|
+
await executeVerifyQualityScan({ cwd, specBase, changeName, platformOpts })
|
|
418
|
+
} else if (_cliAction === 'archiveDistill') {
|
|
419
|
+
// 与 stage.js noAI 分支同语义(P0-4 安全变体:distill 提炼 CLI 机械执行,needsWait 裁决
|
|
420
|
+
// 收敛到「确认归档 --confirm」;全路径不抛)
|
|
421
|
+
const { executeArchiveDistill } = await import('./archive-distill.js')
|
|
422
|
+
await executeArchiveDistill({ cwd, specBase, changeName })
|
|
383
423
|
} else if (_cliAction === 'doctorRunDiagnostics') {
|
|
384
424
|
// 2026-09-09-doctor-noai:与 stage.js 分支同语义(诊断+渲染+落盘)
|
|
385
425
|
const { runDoctorDiagnostics, renderDoctorSummary, writeDoctorDiagnosis } = await import('../doctor-diagnostics.js')
|
|
@@ -622,6 +662,21 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
622
662
|
const total = steps.length
|
|
623
663
|
console.log(`✅ ${stageName} 阶段已完成(${total}/${total} 步)`)
|
|
624
664
|
|
|
665
|
+
// task-04(2026-09-10-change-scope-audit):execute 完成打范围对账全表+落 .runtime 快照 /
|
|
666
|
+
// verify 完成出一行漂移确认。advisory fail-soft(D-006)——helper 内部全兜,绝不阻断完成。
|
|
667
|
+
await printStageCompletionScopeAudit({ stageName, cwd, changeName, specBase, platformOpts })
|
|
668
|
+
|
|
669
|
+
// 摩擦信号消费(friction-signal-hint task-03):verify 收尾读一次 friction tally,非零打
|
|
670
|
+
// 一行 advisory 引导补 postmortem(consume 内部提示后清零);全零/读失败零输出——
|
|
671
|
+
// 摩擦提示失败不影响收尾。
|
|
672
|
+
if (stageName === 'verify') {
|
|
673
|
+
try {
|
|
674
|
+
const { consumeFrictionHint } = await import('../friction-tally.js')
|
|
675
|
+
const r = consumeFrictionHint({ cwd, changeName, platformOpts })
|
|
676
|
+
if (r && r.hint) console.log(`\n${r.hint}`)
|
|
677
|
+
} catch { /* 摩擦提示失败不影响收尾 */ }
|
|
678
|
+
}
|
|
679
|
+
|
|
625
680
|
if (stageName === 'execute') {
|
|
626
681
|
// execute run summary:展示真实可得的结构化信息
|
|
627
682
|
try {
|
|
@@ -666,6 +721,7 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
666
721
|
// 走下方 _getNextSuggestion 会因 scan 是 STAGE_ORDER 首位、永未完成而误推 scan(回头路),
|
|
667
722
|
// 与 brainstorm 同类问题(见上注释),故给 quick 专属分支。
|
|
668
723
|
console.log('\n👉 quick 已完成。下一步:提交本次改动(git commit / sillyspec-commit),或 sillyspec run <stage> 继续其他阶段。')
|
|
724
|
+
console.log(' ⚠️ 多会话共享仓禁目录级 git add / git add -A(会夹带并行会话文件,2026-09-10 实证误删已提交文档):用显式 pathspec(git add -- 文件…),提交前 git status 核对暂存面。')
|
|
669
725
|
} else {
|
|
670
726
|
// D1 暗衔接修:阶段刚完成,CLI 本就知道状态机下一步是哪个阶段(_getNextSuggestion),
|
|
671
727
|
// 却只丢一句「下一步由你决定」让 agent 自己猜命令。改为按 progress 实际状态给出精确命令。
|
|
@@ -729,6 +785,106 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
729
785
|
return { stageCompleted: false, currentIdx, nextPendingIdx }
|
|
730
786
|
}
|
|
731
787
|
|
|
788
|
+
// ── 变更范围对账 advisory 注入(2026-09-10-change-scope-audit task-04,FR-03)──
|
|
789
|
+
// execute 完成打全表+落快照、verify 完成出一行漂移确认;completeStep 与 continueStep(wait
|
|
790
|
+
// 解除完成)两路径收敛点同调 printStageCompletionScopeAudit(Grill G-2:单锚 completeStep 会漏
|
|
791
|
+
// wait 解除路径致 verify 假报无快照)。全程 fail-soft(D-006):任何异常只打单行 ⚠️,不阻断
|
|
792
|
+
// 阶段完成、不进任何门禁判定。scope-audit.js 经动态 import 接入(本文件 runtime-hygiene /
|
|
793
|
+
// design-facts 等同款先例)——只读纯函数模块,模块缓存下零重复加载。
|
|
794
|
+
// - execute:renderScopeAuditTable 全表(maxRows 60 截断 + 完整表跑 scope-audit 指引 +
|
|
795
|
+
// ⚠️ 出口指引行——计划外补 design.md 声明或 --output 注明原因,均 renderer 内置)+ 落
|
|
796
|
+
// .runtime/scope-audit-<change>.json 快照(结构即 ScopeAuditResult + savedAt 落盘时间戳,
|
|
797
|
+
// 只写 .runtime 运行时产物区不动 git 状态)。
|
|
798
|
+
// - verify:读快照对比文件集差集出一行「变更范围:N 文件 +X/-Y(vs execute 时点:一致|漂移
|
|
799
|
+
// M 文件:<列表截断>)」。漂移过滤面 = filterDeliverableFiles 结果(resolveReconcileActualFiles
|
|
800
|
+
// 已过、本就保留 docs 子树)再排除 .sillyspec/docs/**——verify 阶段合法文档同步不计漂移
|
|
801
|
+
// (Grill C-5/P2-②:组合过滤在本侧做,不动 worktree-apply.js 过滤器本体)。快照缺失/降级 →
|
|
802
|
+
// 跳过对比只打当前 totals 一行。
|
|
803
|
+
|
|
804
|
+
/** 漂移对比排除面:verify 阶段合法文档同步子树(posix 归一后前缀匹配) */
|
|
805
|
+
const SCOPE_DRIFT_EXCLUDE_RE = /^\.sillyspec\/docs\//
|
|
806
|
+
|
|
807
|
+
/** 快照/当前结果 → 漂移对比用文件集(posix 归一 + docs 子树排除);畸形结构防御式得空集 */
|
|
808
|
+
function scopeAuditPathSet(result) {
|
|
809
|
+
const rows = Array.isArray(result?.rows) ? result.rows : []
|
|
810
|
+
const set = new Set()
|
|
811
|
+
for (const r of rows) {
|
|
812
|
+
const p = r && typeof r.path === 'string' ? r.path.replace(/\\/g, '/') : ''
|
|
813
|
+
if (p && !SCOPE_DRIFT_EXCLUDE_RE.test(p)) set.add(p)
|
|
814
|
+
}
|
|
815
|
+
return set
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/** execute 完成路径:全表 + 审计级快照/patch 落盘(quick-359a48f1:changes/<变更名>/ 随归档
|
|
819
|
+
* 入库;patch 为收尾时点全量冻结——--file 真·当时内容比对的数据源。写失败只提示,verify
|
|
820
|
+
* 对比按无快照降级) */
|
|
821
|
+
async function printExecuteScopeAudit({ cwd, changeName, specBase, platformOpts, computeChangeScopeAudit, renderScopeAuditTable }) {
|
|
822
|
+
const result = await computeChangeScopeAudit({ cwd, specBase, changeName, platformOpts, collectPatch: true })
|
|
823
|
+
console.log(`\n${renderScopeAuditTable(result, { maxRows: 60 })}`)
|
|
824
|
+
try {
|
|
825
|
+
const changeDir = join(specBase, 'changes', changeName)
|
|
826
|
+
mkdirSync(changeDir, { recursive: true })
|
|
827
|
+
const { frozenPatch, ...snap } = result
|
|
828
|
+
writeFileSync(join(changeDir, 'scope-audit.json'), JSON.stringify({ ...snap, savedAt: new Date().toISOString() }, null, 2) + '\n')
|
|
829
|
+
if (typeof frozenPatch === 'string') {
|
|
830
|
+
writeFileSync(join(changeDir, 'scope-audit.patch'), frozenPatch.endsWith('\n') ? frozenPatch : frozenPatch + '\n')
|
|
831
|
+
}
|
|
832
|
+
console.log(` 📦 范围快照已落变更目录(scope-audit.json${typeof frozenPatch === 'string' ? ' + scope-audit.patch(收尾时点冻结)' : ''})`)
|
|
833
|
+
} catch (e) {
|
|
834
|
+
console.warn(` ⚠️ 范围对账快照写入失败(不阻断,verify 漂移对比将按无快照降级):${e && e.message ? String(e.message).split('\n')[0] : e}`)
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/** verify 完成路径:读 execute 时点快照对比文件集差集,一行结论(行数不比——execute 形态 A 锚
|
|
839
|
+
* worktree meta、verify 形态 B 锚 merge-base,基点不同行数天然抖动,比行数必假阳) */
|
|
840
|
+
async function printVerifyScopeDrift({ cwd, changeName, specBase, platformOpts, computeChangeScopeAudit }) {
|
|
841
|
+
const result = await computeChangeScopeAudit({ cwd, specBase, changeName, platformOpts })
|
|
842
|
+
const totals = result?.totals && typeof result.totals === 'object' ? result.totals : {}
|
|
843
|
+
const scopeLine = `变更范围:${totals.files ?? (Array.isArray(result?.rows) ? result.rows.length : 0)} 文件 +${totals.additions ?? 0}/-${totals.deletions ?? 0}`
|
|
844
|
+
if (result?.ok !== true) {
|
|
845
|
+
console.log(` ⚠️ ${scopeLine}(对账降级:${result?.degradedReason || '实际侧文件集解析失败'}——advisory,不影响 verify 完成)`)
|
|
846
|
+
return
|
|
847
|
+
}
|
|
848
|
+
let snapshot = null
|
|
849
|
+
// 快照读取链(quick-359a48f1):变更目录(审计级,execute --done 落)→ .runtime(存量兼容)
|
|
850
|
+
try {
|
|
851
|
+
snapshot = JSON.parse(readFileSync(join(specBase, 'changes', changeName, 'scope-audit.json'), 'utf8'))
|
|
852
|
+
} catch {
|
|
853
|
+
try {
|
|
854
|
+
snapshot = JSON.parse(readFileSync(join(resolveRuntimeRoot(platformOpts, specBase), `scope-audit-${changeName}.json`), 'utf8'))
|
|
855
|
+
} catch { /* 快照缺失/损坏 → 跳过对比只打当前 totals */ }
|
|
856
|
+
}
|
|
857
|
+
if (!snapshot || snapshot.ok !== true) {
|
|
858
|
+
console.log(` ${scopeLine}(无 execute 时点快照可对比——完整表跑 sillyspec scope-audit --change ${changeName})`)
|
|
859
|
+
return
|
|
860
|
+
}
|
|
861
|
+
const current = scopeAuditPathSet(result)
|
|
862
|
+
const baseline = scopeAuditPathSet(snapshot)
|
|
863
|
+
const drift = [...new Set([...current, ...baseline])].filter(p => current.has(p) !== baseline.has(p)).sort()
|
|
864
|
+
if (drift.length === 0) {
|
|
865
|
+
console.log(` ${scopeLine}(vs execute 时点:一致)`)
|
|
866
|
+
} else {
|
|
867
|
+
const shown = drift.slice(0, 5).join(', ')
|
|
868
|
+
const tail = drift.length > 5 ? ` 等 ${drift.length} 个` : ''
|
|
869
|
+
console.log(` ${scopeLine}(vs execute 时点:漂移 ${drift.length} 文件:${shown}${tail}——advisory 不阻断,请确认 verify 期改动并按需补 design.md 声明)`)
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/** 两完成路径公共入口(stage 派发 + 动态 import + fail-soft 兜底,D-006) */
|
|
874
|
+
async function printStageCompletionScopeAudit({ stageName, cwd, changeName, specBase, platformOpts }) {
|
|
875
|
+
try {
|
|
876
|
+
if (!changeName || (stageName !== 'execute' && stageName !== 'verify')) return
|
|
877
|
+
const { computeChangeScopeAudit, renderScopeAuditTable } = await import('../scope-audit.js')
|
|
878
|
+
if (stageName === 'execute') {
|
|
879
|
+
await printExecuteScopeAudit({ cwd, changeName, specBase, platformOpts, computeChangeScopeAudit, renderScopeAuditTable })
|
|
880
|
+
} else {
|
|
881
|
+
await printVerifyScopeDrift({ cwd, changeName, specBase, platformOpts, computeChangeScopeAudit })
|
|
882
|
+
}
|
|
883
|
+
} catch (e) {
|
|
884
|
+
console.warn(`⚠️ 变更范围对账输出异常(advisory,不阻断 ${stageName} 阶段完成):${e && e.message ? String(e.message).split('\n')[0] : e}`)
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
732
888
|
// ── Step 调度兄弟函数(W6 Step7b-2 从 run.js 搬入:skip/wait/continue + formatWaitHistory)──
|
|
733
889
|
/**
|
|
734
890
|
* plan.md checkbox 自动勾选:execute 完成 + 各 task review.json 双 verdict 非 fail → 自动勾选。
|
|
@@ -858,7 +1014,7 @@ export function shouldAutoCheckTask(r, endToEnd, ctx = null) {
|
|
|
858
1014
|
|
|
859
1015
|
return true
|
|
860
1016
|
}
|
|
861
|
-
async function autoCheckPlanFromReviews({ stageName, changeName, cwd, platformOpts }) {
|
|
1017
|
+
export async function autoCheckPlanFromReviews({ stageName, changeName, cwd, platformOpts }) {
|
|
862
1018
|
if (stageName !== 'execute' || !changeName) {
|
|
863
1019
|
return { autoChecked: false, checkedCount: 0, skippedCount: 0 }
|
|
864
1020
|
}
|
|
@@ -1337,6 +1493,18 @@ export async function continueStep(pm, progress, stageName, cwd, answer, options
|
|
|
1337
1493
|
// gate 全过:persist completed(task-03 移后;此处无 triggerSync)。
|
|
1338
1494
|
pm._write(cwd, progress, changeName)
|
|
1339
1495
|
console.log(`\n✅ ${stageName} 阶段已完成(${stageData.steps.length}/${stageData.steps.length} 步)`)
|
|
1496
|
+
// task-04:wait 解除完成路径与 completeStep 完成分支同源注入(Grill G-2:本路径不漏,
|
|
1497
|
+
// 否则 execute 快照缺失 → verify 假报「无快照可对比」)。
|
|
1498
|
+
await printStageCompletionScopeAudit({ stageName, cwd, changeName, specBase, platformOpts })
|
|
1499
|
+
// 摩擦信号消费(friction-signal-hint task-03):与 completeStep 完成分支同款(Grill CC-03:
|
|
1500
|
+
// 漏此则 --continue 收尾丢提示丢清零)。全零/读失败零输出,摩擦提示失败不影响收尾。
|
|
1501
|
+
if (stageName === 'verify') {
|
|
1502
|
+
try {
|
|
1503
|
+
const { consumeFrictionHint } = await import('../friction-tally.js')
|
|
1504
|
+
const r = consumeFrictionHint({ cwd, changeName, platformOpts })
|
|
1505
|
+
if (r && r.hint) console.log(`\n${r.hint}`)
|
|
1506
|
+
} catch { /* 摩擦提示失败不影响收尾 */ }
|
|
1507
|
+
}
|
|
1340
1508
|
// 阶段完成后明确下一步(agent 常卡:stageData completed 但不知要 run <下一阶段> 推进 currentStage)
|
|
1341
1509
|
const nextStageHint = { brainstorm: 'plan', plan: 'execute', execute: 'verify', verify: 'archive' }[stageName]
|
|
1342
1510
|
if (nextStageHint) {
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* gate-snapshot.js — quick --done test+lint 门禁的隔离快照执行(2026-09-10 驾驭小结第六批②,
|
|
3
|
+
* 用户实证「lint 实测对账在主仓跑,被并行会话的脏文件拦门」)。
|
|
4
|
+
*
|
|
5
|
+
* 问题:门禁在主仓工作区跑 commands.test/lint——多会话共享仓里并行会话的未提交 src/test
|
|
6
|
+
* 改动(语法错误/半成品/锚漂)会污染实测结果,把无辜会话的 --done 拦在门上(只能走
|
|
7
|
+
* advisory 逃生口)。
|
|
8
|
+
*
|
|
9
|
+
* 修法:HEAD 干净快照 + 会话文件 overlay——`git worktree add --detach <tmp> HEAD` 取干净
|
|
10
|
+
* 基线(并行脏文件天然不在),把本会话审计/声明的变更文件覆盖进快照,node_modules 经
|
|
11
|
+
* junction 复用主仓依赖(零拷贝),local.yaml 从主仓复制(gitignore 不进 HEAD),在快照
|
|
12
|
+
* cwd 跑对账。快照基建任何失败 → 返回 null,调用方回退主仓现行为(零回归兜底)。
|
|
13
|
+
*
|
|
14
|
+
* 快照生命周期:一次门禁一批(create → run → cleanup);崩溃残留由 git worktree prune /
|
|
15
|
+
* 临时目录自然回收(OS tmp 清理),不进主仓 .runtime。
|
|
16
|
+
*/
|
|
17
|
+
import { execFileSync } from 'node:child_process'
|
|
18
|
+
import { mkdtempSync, mkdirSync, copyFileSync, writeFileSync, rmSync, existsSync, readdirSync, statSync, symlinkSync, readFileSync } from 'node:fs'
|
|
19
|
+
import { join, dirname, relative, resolve } from 'node:path'
|
|
20
|
+
import { tmpdir } from 'node:os'
|
|
21
|
+
|
|
22
|
+
function git(cwd, args) {
|
|
23
|
+
return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 60000 }).trim()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 创建隔离快照:HEAD worktree + 会话文件 overlay + node_modules junction + local.yaml。
|
|
28
|
+
* @param {{ cwd: string, files: string[] }} opts cwd=主仓根;files=本会话变更文件(仓库根相对 POSIX)
|
|
29
|
+
* @returns {{ snapshotRoot: string, cleanup: () => void, reason?: string }|null} 失败返回 null(调用方回退主仓)
|
|
30
|
+
*/
|
|
31
|
+
export function createGateSnapshot({ cwd, files, sourceRoot = null }) {
|
|
32
|
+
let snapshotRoot = null
|
|
33
|
+
try {
|
|
34
|
+
// 前置:主仓须是 git 仓且有 HEAD(无 git 环境回退主仓现行为)
|
|
35
|
+
git(cwd, ['rev-parse', 'HEAD'])
|
|
36
|
+
|
|
37
|
+
snapshotRoot = mkdtempSync(join(tmpdir(), 'sillyspec-gate-'))
|
|
38
|
+
git(cwd, ['worktree', 'add', '--detach', '--quiet', snapshotRoot, 'HEAD'])
|
|
39
|
+
|
|
40
|
+
// 会话文件 overlay:主仓工作区版本覆盖进快照(本会话的最新态;文件不存在=已删,跳过)。
|
|
41
|
+
// sourceRoot(verify 门定向跑用,2026-09-10 驾驭小结第八批):overlay 源切换为本变更
|
|
42
|
+
// worktree 根——「--worktree 定向跑本变更分支内容」的快照实现(缺省仍是主仓 cwd)。
|
|
43
|
+
// .sillyspec/ 跳过面收窄(P2-d 落地实证的快照盲区):原一刀切跳过整个 .sillyspec/ ——
|
|
44
|
+
// 会话声明的 tracked docs(module-map 补录/知识库/模块卡)进不了快照,门禁读到 HEAD
|
|
45
|
+
// 旧版恒误报(src 未录 module-map 的修复在主仓生效、快照里仍然失败)。真正需要隔离的
|
|
46
|
+
// 是共享运行时面:.runtime/(sqlite 锁/在途 marker)与 quicklog/(跨会话共享账本);
|
|
47
|
+
// local.yaml 由下方 cfg 段显式复制(单一来源,不经 overlay)。
|
|
48
|
+
const overlayRoot = sourceRoot || cwd
|
|
49
|
+
let overlaid = 0
|
|
50
|
+
for (const f of files) {
|
|
51
|
+
const isSillyspecRuntime = typeof f === 'string' && (f.startsWith('.sillyspec/.runtime/') || f.startsWith('.sillyspec/quicklog/') || f === '.sillyspec/local.yaml' || f === '.sillyspec/.sillyspec-platform.json')
|
|
52
|
+
if (typeof f !== 'string' || f.includes('..') || f.startsWith('/') || isSillyspecRuntime) continue
|
|
53
|
+
const src = join(overlayRoot, f)
|
|
54
|
+
if (!existsSync(src)) continue
|
|
55
|
+
const dst = join(snapshotRoot, f)
|
|
56
|
+
mkdirSync(dirname(dst), { recursive: true })
|
|
57
|
+
copyFileSync(src, dst)
|
|
58
|
+
overlaid++
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// node_modules junction(依赖主仓安装态;junction Windows 无需特权,POSIX 用 symlink)
|
|
62
|
+
const nmSrc = join(cwd, 'node_modules')
|
|
63
|
+
if (existsSync(nmSrc)) {
|
|
64
|
+
try { symlinkSync(nmSrc, join(snapshotRoot, 'node_modules'), 'junction') }
|
|
65
|
+
catch { /* 依赖链接失败:test/lint 可能跑不起来 → 仍返回快照,命令层报错走 fallback */ }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// local.yaml(gitignore 不进 HEAD;门禁命令配置来源)+ package-lock 保持 HEAD 版(npm test 不装新依赖)
|
|
69
|
+
for (const cfg of [join('.sillyspec', 'local.yaml')]) {
|
|
70
|
+
const src = join(cwd, cfg)
|
|
71
|
+
if (existsSync(src)) {
|
|
72
|
+
mkdirSync(dirname(join(snapshotRoot, cfg)), { recursive: true })
|
|
73
|
+
copyFileSync(src, join(snapshotRoot, cfg))
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const cleanup = () => {
|
|
78
|
+
try { git(cwd, ['worktree', 'remove', '--force', '--quiet', snapshotRoot]) } catch {
|
|
79
|
+
try { rmSync(snapshotRoot, { recursive: true, force: true }) } catch { /* 残留交 OS tmp 清理 */ }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return { snapshotRoot, cleanup, overlaid }
|
|
83
|
+
} catch (e) {
|
|
84
|
+
// 基建失败(worktree add 拒绝/磁盘满/…):尽力清理后回退主仓
|
|
85
|
+
if (snapshotRoot) {
|
|
86
|
+
try { git(cwd, ['worktree', 'remove', '--force', '--quiet', snapshotRoot]) } catch {}
|
|
87
|
+
try { rmSync(snapshotRoot, { recursive: true, force: true }) } catch {}
|
|
88
|
+
}
|
|
89
|
+
return null
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* verify 门专用快照(2026-09-10 驾驭小结第八批,用户第二次真实阻塞:verify 实测门跑
|
|
95
|
+
* main 工作区,多会话并发任何人的 WIP 都能弄红别人的门)。等价「--worktree 定向跑」:
|
|
96
|
+
* overlay 文件集 = resolveVerifyChangedFiles(worktree-aware:含 merge-base 补齐与
|
|
97
|
+
* working-tree),overlay 源 = 本变更 worktree 根(meta 感知;in-place 退主仓 cwd)——
|
|
98
|
+
* 并行会话的在途文件物理不进快照。变更文档(changes/<name>/**,test_strategy 的
|
|
99
|
+
* module-impact.md 等消费)从主仓 specBase 随快照复制。
|
|
100
|
+
* @param {{ cwd: string, changeName: string, specBase: string, platformOpts?: object }} opts
|
|
101
|
+
* @returns {Promise<{snapshotRoot: string, cleanup: () => void, overlaid: number, changeFileCount: number}|null>}
|
|
102
|
+
*/
|
|
103
|
+
export async function createVerifyGateSnapshot({ cwd, changeName, specBase, platformOpts = {} }) {
|
|
104
|
+
try {
|
|
105
|
+
const { resolveVerifyChangedFiles } = await import('../verify-postcheck.js')
|
|
106
|
+
const { resolveRuntimeRoot } = await import('./shared.js')
|
|
107
|
+
const { splitOwnVsForeignDiffFiles } = await import('../foreign-declared.js')
|
|
108
|
+
let changeFiles = resolveVerifyChangedFiles(cwd, changeName, null, {
|
|
109
|
+
specBase, includeWorkingTree: true,
|
|
110
|
+
}) || []
|
|
111
|
+
if (changeFiles.length === 0) return null // 无变更文件集 → 无从定向,回退主仓
|
|
112
|
+
// 他者声明归属过滤(坑 verify-reconcile-foreign-wip 同款):in-place 模式 working-tree
|
|
113
|
+
// 并入后主仓全部在途文件进场——他者活跃变更显式声明的文件(quick --files / 他者 design
|
|
114
|
+
// 清单)剔除,只留本变更相关面(无主文件保留,fail-closed 口径与 probe6 一致)。
|
|
115
|
+
try {
|
|
116
|
+
const runtimeRoot = resolveRuntimeRoot(platformOpts, specBase)
|
|
117
|
+
const { foreign } = splitOwnVsForeignDiffFiles(cwd, changeName, changeFiles, { specBase, runtimeRoot })
|
|
118
|
+
if (foreign.length > 0) {
|
|
119
|
+
const foreignSet = new Set(foreign.map(x => x.file))
|
|
120
|
+
changeFiles = changeFiles.filter(f => !foreignSet.has(f))
|
|
121
|
+
if (changeFiles.length === 0) return null
|
|
122
|
+
}
|
|
123
|
+
} catch { /* 过滤失败退全量(fail-closed:宁可多 overlay 不漏本变更文件) */ }
|
|
124
|
+
|
|
125
|
+
// overlay 源:worktree meta 感知(native worktree 在则从 worktree 根取「本变更分支内容」)
|
|
126
|
+
let sourceRoot = null
|
|
127
|
+
try {
|
|
128
|
+
const { WorktreeManager } = await import('../worktree.js')
|
|
129
|
+
const wm = new WorktreeManager({ cwd })
|
|
130
|
+
const meta = wm.getMeta(changeName)
|
|
131
|
+
if (meta && meta.worktreePath && meta.mode !== 'in-place-fallback' && existsSync(meta.worktreePath)) {
|
|
132
|
+
sourceRoot = meta.worktreePath
|
|
133
|
+
}
|
|
134
|
+
} catch { /* meta 读取失败 → 主仓 cwd 源(files 已含本变更 working-tree 改动) */ }
|
|
135
|
+
|
|
136
|
+
const snap = createGateSnapshot({ cwd, files: changeFiles, sourceRoot })
|
|
137
|
+
if (!snap) return null
|
|
138
|
+
|
|
139
|
+
// 变更文档随快照:module-impact.md(test_strategy=evidence-auto 消费)/tasks.md 等
|
|
140
|
+
// 在 .sillyspec(gitignore)不进 HEAD,从主仓 specBase 复制 changes/<name>/**
|
|
141
|
+
try {
|
|
142
|
+
const changeDir = join(specBase, 'changes', changeName)
|
|
143
|
+
if (existsSync(changeDir)) {
|
|
144
|
+
const copyDir = (srcDir, dstDir) => {
|
|
145
|
+
for (const e of readdirSync(srcDir, { withFileTypes: true })) {
|
|
146
|
+
const s = join(srcDir, e.name); const d = join(dstDir, e.name)
|
|
147
|
+
if (e.isDirectory()) { mkdirSync(d, { recursive: true }); copyDir(s, d) }
|
|
148
|
+
else { mkdirSync(dirname(d), { recursive: true }); copyFileSync(s, d) }
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
copyDir(changeDir, join(snap.snapshotRoot, '.sillyspec', 'changes', changeName))
|
|
152
|
+
}
|
|
153
|
+
} catch { /* 文档复制失败不连坐快照(test_strategy 降级 module 集仍可跑) */ }
|
|
154
|
+
|
|
155
|
+
return { ...snap, changeFileCount: changeFiles.length, sourceRoot }
|
|
156
|
+
} catch {
|
|
157
|
+
return null // 任一链路异常 → 回退主仓现行为
|
|
158
|
+
}
|
|
159
|
+
}
|