sillyspec 3.28.7 → 3.28.8
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 +224 -222
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.claude/skills/sillyspec-quick/SKILL.md +117 -116
- package/.husky/pre-push +3 -1
- package/.patch16-tmp.mjs +169 -0
- package/package.json +1 -1
- package/src/agent-session-log.js +446 -35
- package/src/config-cat.js +6 -3
- package/src/constants.js +3 -0
- package/src/db.js +12 -3
- package/src/docs-check.js +137 -12
- package/src/docs-gate.js +3 -0
- package/src/doctor-diagnostics.js +19 -7
- package/src/friction-tally.js +7 -2
- package/src/git-helper.js +6 -4
- package/src/hooks/worktree-guard.js +59 -29
- package/src/index.js +168 -64
- package/src/local-detect.js +30 -15
- package/src/local-register.js +13 -4
- package/src/mcp-server.js +38 -8
- package/src/progress/change-registry.js +41 -35
- package/src/progress/consistency-doctor.js +11 -4
- package/src/progress/stage-machine.js +3 -1
- package/src/progress/step-store.js +30 -17
- package/src/progress.js +23 -0
- package/src/quicklog.js +44 -18
- package/src/run/command.js +17 -3
- package/src/run/complete-handlers.js +3 -3
- package/src/run/complete.js +28 -9
- package/src/run/concurrent-detect.js +1 -1
- package/src/run/gate-snapshot.js +115 -5
- package/src/run/gates.js +35 -5
- package/src/run/prompt.js +9 -1
- package/src/run/quick-audit.js +25 -2
- package/src/run/shared.js +98 -10
- package/src/run/verify-quality-scan.js +14 -3
- package/src/spec-sync.js +19 -14
- package/src/stage-contract-spec.js +4 -3
- package/src/stage-review.js +6 -5
- package/src/stages/brainstorm.js +1 -1
- package/src/stages/execute.js +6 -1
- package/src/stages/plan-postcheck.js +57 -12
- package/src/sync.js +66 -4
- package/src/task-review.js +41 -6
- package/src/verify-facts-schema.js +10 -3
- package/src/verify-postcheck.js +21 -6
- package/src/verify-probes.js +8 -3
- package/src/worktree-apply.js +56 -18
- package/src/worktree-cross.js +12 -18
- package/src/worktree.js +138 -25
- package/src/wt-commit.js +128 -0
package/src/run/command.js
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import { basename, join, resolve, dirname } from 'node:path'
|
|
23
23
|
import { existsSync, readdirSync, mkdirSync, writeFileSync, readFileSync, rmSync, unlinkSync } from 'node:fs'
|
|
24
24
|
import { randomBytes, randomUUID } from 'node:crypto'
|
|
25
|
+
import { fileURLToPath } from 'node:url'
|
|
25
26
|
import { writeAtomicSync } from '../fs-atomic.js'
|
|
26
27
|
import { resolveSpecDir, countAncestorSpecDirs, ancestorSpecDirs, resolveAncestorCeiling, resolveChangeDir, triggerSync, getStageSteps, formatWaitOptions, checkApproval, warnApprovalUnknown, didYouMean, assertSafeChangeName, assertDatedChangeName, detectQuickSessionDrift, detectWorktreeSpecDrift, resolveRuntimeRoot, resolveQuickSessionsDir, writePlatformPointer, checkPlatformManaged, isSelfReferentialSpecRoot, isTempResidueSpecRoot, PLATFORM_MANAGED_FILENAME, warnSelfRefPointerOnce } from './shared.js'
|
|
27
28
|
import { resolveQuickLinkedChanges } from './quick-audit.js'
|
|
@@ -819,6 +820,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
819
820
|
const knownFlags = new Set([
|
|
820
821
|
'--done', '--skip', '--status', '--reset', '--confirm', '--skip-approval', '--cancel', '--ql',
|
|
821
822
|
'--wait', '--continue', '--non-interactive', '--interactive',
|
|
823
|
+
'--wait-interactive', // auto 模式 TTY 直通(带值形态 `--wait-interactive true`,autoFlagValue 消费;ql-20260911-029 补注册——此前漏登记,进命令前即被未知参数 exit(2) 拦死,FR-03 整条死路)
|
|
822
824
|
'--reason', '--options', '--answer', '--confirm-mode',
|
|
823
825
|
'--output', '--input', '--change', '--linked-changes',
|
|
824
826
|
'--req', '--cause', '--solution', '--result',
|
|
@@ -1286,11 +1288,14 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
1286
1288
|
console.error('用法: sillyspec run quick --cancel --change <quick-会话ID> [--ql <ql-xxx>]\n 取消误启动的 quick 会话:QUICKLOG 条目翻「已取消」+ tasks.md 挂载行移除 + 会话目录清理 + db 行注销。\n 已完成/已勾选的会话拒绝取消(真实工作记录)')
|
|
1287
1289
|
process.exit(2)
|
|
1288
1290
|
}
|
|
1289
|
-
|
|
1291
|
+
// 平台模式对齐(坑 quick-cancel-platform-specbase,2026-09-12 审查批 B-④):此处原用
|
|
1292
|
+
// resolveSpecDir 本地重建 specBase,遮蔽外层平台感知 specBase(:476 specRoot 优先)——
|
|
1293
|
+
// 平台模式 guard 恒读不到、QUICKLOG 翻态落错库。guard 查找同其他收敛点走
|
|
1294
|
+
// resolveQuickSessionsDir(runtimeRoot 可与 specBase/.runtime 异位,multi-agent-review Q4)。
|
|
1290
1295
|
// qlId 优先 --ql 显式;否则读会话 guard.json(allocate 时 CLI 写入);再退 current marker 旁的会话
|
|
1291
1296
|
let qlId = qlFlag
|
|
1292
1297
|
if (!qlId) {
|
|
1293
|
-
const guardPath = join(
|
|
1298
|
+
const guardPath = join(resolveQuickSessionsDir(platformOpts, specBase), cancelSessionRaw, 'guard.json')
|
|
1294
1299
|
try {
|
|
1295
1300
|
if (existsSync(guardPath)) {
|
|
1296
1301
|
// 字段名 bug 修复(2026-09-09 §7-5,会话实证:guard 在场仍报「缺失/损坏」):
|
|
@@ -1896,6 +1901,15 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
|
|
|
1896
1901
|
* 无新状态);任一不满足/异常 → 静默或提示回三段式(fail-open,Grill P2-①时序)。
|
|
1897
1902
|
* 仅 runAutoMode 的首渲染点消费(--done 推进路径不重收)。
|
|
1898
1903
|
*/
|
|
1904
|
+
/**
|
|
1905
|
+
* 本 CLI 入口(bin/sillyspec.js)的绝对路径——wait 直通子进程自调用。
|
|
1906
|
+
* fileURLToPath 解 URL(ql-20260911-029:旧 pathname.replace 手解百分号编码,含空格/中文
|
|
1907
|
+
* 路径 ENOENT;且旧相对层级 ../bin 少一级——src/run/ 到仓根 bin 应为 ../../bin)。导出供单测锁层级。
|
|
1908
|
+
*/
|
|
1909
|
+
export function resolveBinSelfPath() {
|
|
1910
|
+
return fileURLToPath(new URL('../../bin/sillyspec.js', import.meta.url))
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1899
1913
|
export async function maybeWaitInteractive(stepDef, changeName, stageName, { rlFactory = null } = {}) {
|
|
1900
1914
|
try {
|
|
1901
1915
|
if (!globalThis.__ssWaitInteractive) return
|
|
@@ -1920,7 +1934,7 @@ export async function maybeWaitInteractive(stepDef, changeName, stageName, { rlF
|
|
|
1920
1934
|
console.log(`📩 已接收,自动续行:sillyspec run auto --continue --answer "${answer.trim().slice(0, 60)}${answer.trim().length > 60 ? '…' : ''}" ...`)
|
|
1921
1935
|
// 复用既有 continue 路径:与 runCommand 的 --continue 同源(子进程自调避免递归锁)
|
|
1922
1936
|
const { execFileSync } = await import('node:child_process')
|
|
1923
|
-
const binSelf =
|
|
1937
|
+
const binSelf = resolveBinSelfPath()
|
|
1924
1938
|
try {
|
|
1925
1939
|
execFileSync(process.execPath, [binSelf, 'run', 'auto', '--continue', '--answer', answer.trim(), '--change', changeName], { stdio: 'inherit', cwd: process.cwd() })
|
|
1926
1940
|
} catch { /* continue 失败(非 waiting 态等)静默——回三段式由 agent 走 */ }
|
|
@@ -1067,7 +1067,7 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
|
|
|
1067
1067
|
if (outputText) steps[currentIdx].output = null
|
|
1068
1068
|
// 摩擦计数(friction-signal-hint task-04):quick 审计被拦也是摩擦信号——exit 前记
|
|
1069
1069
|
// 一笔(changeName=quick 会话 id,tally 落 session 目录,成功收尾 consume 后随目录清理)。
|
|
1070
|
-
recordFrictionEvent({ cwd, changeName, platformOpts, type: 'gate_rollback', detail: 'quick-audit' })
|
|
1070
|
+
await recordFrictionEvent({ cwd, changeName, platformOpts, type: 'gate_rollback', detail: 'quick-audit' })
|
|
1071
1071
|
process.exit(1)
|
|
1072
1072
|
}
|
|
1073
1073
|
// P0-2(2026-09-02 跨 agent 工单):触及 src/test 的 quick --done 内置 test+lint
|
|
@@ -1090,7 +1090,7 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
|
|
|
1090
1090
|
if (outputText) steps[currentIdx].output = null
|
|
1091
1091
|
// 摩擦计数(friction-signal-hint task-04):test/lint 实测门禁拦下同记摩擦——exit 前
|
|
1092
1092
|
// 落 session tally,与 quick-audit 同款静默降级。
|
|
1093
|
-
recordFrictionEvent({ cwd, changeName, platformOpts, type: 'gate_rollback', detail: 'quick-test-lint' })
|
|
1093
|
+
await recordFrictionEvent({ cwd, changeName, platformOpts, type: 'gate_rollback', detail: 'quick-test-lint' })
|
|
1094
1094
|
process.exit(1)
|
|
1095
1095
|
}
|
|
1096
1096
|
progress.lastQuickReview = review
|
|
@@ -1367,7 +1367,7 @@ export async function handleQuickStageCompletion({ stageName, steps, currentIdx,
|
|
|
1367
1367
|
// tally 随目录删除,晚于此就读不到。QUICKLOG 完成打印之后 consume,提示里的 postmortem
|
|
1368
1368
|
// 建议有落点可循;全零/读失败零输出,摩擦提示失败不影响收尾。
|
|
1369
1369
|
try {
|
|
1370
|
-
const fr = consumeFrictionHint({ cwd, changeName, platformOpts })
|
|
1370
|
+
const fr = await consumeFrictionHint({ cwd, changeName, platformOpts })
|
|
1371
1371
|
if (fr && fr.hint) console.log(`\n${fr.hint}`)
|
|
1372
1372
|
} catch { /* 摩擦提示失败不影响收尾 */ }
|
|
1373
1373
|
|
package/src/run/complete.js
CHANGED
|
@@ -673,7 +673,7 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
673
673
|
if (stageName === 'verify') {
|
|
674
674
|
try {
|
|
675
675
|
const { consumeFrictionHint } = await import('../friction-tally.js')
|
|
676
|
-
const r = consumeFrictionHint({ cwd, changeName, platformOpts })
|
|
676
|
+
const r = await consumeFrictionHint({ cwd, changeName, platformOpts })
|
|
677
677
|
if (r && r.hint) console.log(`\n${r.hint}`)
|
|
678
678
|
} catch { /* 摩擦提示失败不影响收尾 */ }
|
|
679
679
|
}
|
|
@@ -1071,8 +1071,14 @@ export async function autoCheckPlanFromReviews({ stageName, changeName, cwd, pla
|
|
|
1071
1071
|
const taskNum = match.match(/task-(\d+)/)[1].padStart(2, '0')
|
|
1072
1072
|
const reviewPath = join(runtimeRoot, 'execute-runs', executeRunId, 'tasks', `task-${taskNum}`, 'review.json')
|
|
1073
1073
|
const r = readReview(reviewPath)
|
|
1074
|
-
//
|
|
1075
|
-
|
|
1074
|
+
// 卡片读取延后到 verdict 可用之后(2026-09-11 审查性能包③):review 缺失或 spec/quality
|
|
1075
|
+
// 已 fail 时 endToEnd 取值不影响 shouldAutoCheckTask 结果(恒 false),先读卡片是纯浪费
|
|
1076
|
+
// ——持 tasks 锁期间每 task 省一次 readFileSync。
|
|
1077
|
+
const verdictUsable = r?.ok
|
|
1078
|
+
&& r.review?.specVerdict !== 'fail'
|
|
1079
|
+
&& r.review?.qualityVerdict !== 'fail'
|
|
1080
|
+
const endToEnd = verdictUsable
|
|
1081
|
+
&& isEndToEndTaskText(match + ' ' + readTaskCardText(changeDir, taskNum))
|
|
1076
1082
|
if (shouldAutoCheckTask(r, endToEnd, ctx)) {
|
|
1077
1083
|
checkedCount++
|
|
1078
1084
|
return `${p1}x${p2}` // 勾选
|
|
@@ -1212,6 +1218,11 @@ async function detectExecuteBatchFinish({ pm, stageName, changeName, cwd, specBa
|
|
|
1212
1218
|
if (step.status === 'pending' || step.status === 'in-progress') {
|
|
1213
1219
|
step.status = 'completed'
|
|
1214
1220
|
step.completedAt = now
|
|
1221
|
+
// 乐观预标戳:这是本次 --done 的批量乐观标记而非逐步验证的完成。任一收尾 gate 失败时
|
|
1222
|
+
// rollbackStageCompletion 据此一并回滚(坑 execute-batch-rollback-half-state:只回滚
|
|
1223
|
+
// currentIdx 会落库「步骤 11 pending + 12-16 completed」半套状态)。内存字段,_write 只写
|
|
1224
|
+
// 固定列不落库。
|
|
1225
|
+
step._batchAligned = true
|
|
1215
1226
|
aligned++
|
|
1216
1227
|
}
|
|
1217
1228
|
}
|
|
@@ -1251,10 +1262,11 @@ export async function waitStep(pm, progress, stageName, cwd, outputText, waitRea
|
|
|
1251
1262
|
process.exit(1)
|
|
1252
1263
|
}
|
|
1253
1264
|
|
|
1254
|
-
// 查找下一个 pending
|
|
1255
|
-
|
|
1265
|
+
// 查找下一个 pending / in-progress / blocked 的步骤(blocked 同为当前步——与 completeStep
|
|
1266
|
+
// 的 findIndex 口径对齐,否则 blocked 后 --wait 落到其后第一个 pending 步上错步记账)
|
|
1267
|
+
const currentIdx = stageData.steps.findIndex(s => s.status === 'pending' || s.status === 'in-progress' || s.status === 'blocked')
|
|
1256
1268
|
if (currentIdx === -1) {
|
|
1257
|
-
console.error(`没有可以等待的步骤(阶段 ${stageName} 已无 pending/in-progress 步骤)。当前阶段状态:${stageData?.status ?? '未知'}。用 \`sillyspec run ${stageName} --status\` 查看进度,或 \`sillyspec progress show\` 看全局下一步。`)
|
|
1269
|
+
console.error(`没有可以等待的步骤(阶段 ${stageName} 已无 pending/in-progress/blocked 步骤)。当前阶段状态:${stageData?.status ?? '未知'}。用 \`sillyspec run ${stageName} --status\` 查看进度,或 \`sillyspec progress show\` 看全局下一步。`)
|
|
1258
1270
|
process.exit(1)
|
|
1259
1271
|
}
|
|
1260
1272
|
|
|
@@ -1513,7 +1525,7 @@ export async function continueStep(pm, progress, stageName, cwd, answer, options
|
|
|
1513
1525
|
if (stageName === 'verify') {
|
|
1514
1526
|
try {
|
|
1515
1527
|
const { consumeFrictionHint } = await import('../friction-tally.js')
|
|
1516
|
-
const r = consumeFrictionHint({ cwd, changeName, platformOpts })
|
|
1528
|
+
const r = await consumeFrictionHint({ cwd, changeName, platformOpts })
|
|
1517
1529
|
if (r && r.hint) console.log(`\n${r.hint}`)
|
|
1518
1530
|
} catch { /* 摩擦提示失败不影响收尾 */ }
|
|
1519
1531
|
}
|
|
@@ -1569,7 +1581,10 @@ export async function skipStep(pm, progress, stageName, cwd, changeName, platfor
|
|
|
1569
1581
|
}
|
|
1570
1582
|
|
|
1571
1583
|
const steps = stageData.steps
|
|
1572
|
-
|
|
1584
|
+
// blocked 同为当前步——与 completeStep 的 findIndex 口径对齐(坑 deps-gate-blocked-invisible
|
|
1585
|
+
// 在 completeStep 修过但此处漏同步:blocked 后 --skip 落到其后第一个 pending 步上,错步跳过
|
|
1586
|
+
// 且 blocked 步永久卡死只能 --reset)
|
|
1587
|
+
const currentIdx = steps.findIndex(s => s.status === 'pending' || s.status === 'in-progress' || s.status === 'blocked')
|
|
1573
1588
|
|
|
1574
1589
|
if (currentIdx === -1) {
|
|
1575
1590
|
const wsIdx = steps.findIndex(s => s.status === 'waiting')
|
|
@@ -1577,11 +1592,15 @@ export async function skipStep(pm, progress, stageName, cwd, changeName, platfor
|
|
|
1577
1592
|
console.error(`⏸️ Step ${wsIdx + 1} 正在等待用户输入,不能跳过。`)
|
|
1578
1593
|
console.error(` 请先使用 --continue --answer "..." 继续,或用 --reset 重置。`)
|
|
1579
1594
|
} else {
|
|
1580
|
-
console.error(`没有待跳过的步骤(阶段 ${stageName} 已无 pending/in-progress 步骤)。当前阶段状态:${stageData?.status ?? '未知'}。用 \`sillyspec run ${stageName} --status\` 查看进度,或 \`sillyspec progress show\` 看全局下一步。`)
|
|
1595
|
+
console.error(`没有待跳过的步骤(阶段 ${stageName} 已无 pending/in-progress/blocked 步骤)。当前阶段状态:${stageData?.status ?? '未知'}。用 \`sillyspec run ${stageName} --status\` 查看进度,或 \`sillyspec progress show\` 看全局下一步。`)
|
|
1581
1596
|
}
|
|
1582
1597
|
process.exit(1)
|
|
1583
1598
|
}
|
|
1584
1599
|
|
|
1600
|
+
if (steps[currentIdx].status === 'blocked') {
|
|
1601
|
+
console.log(`⚠️ Step "${steps[currentIdx].name}" 此前被门控阻断(blocked),本次 --skip 将跳过该步——若门控条件已修复,更宜直接 --done 推进。`)
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1585
1604
|
const defSteps = await getStageStepsAutoAware(stageName, cwd, progress, platformOpts?.specRoot || null)
|
|
1586
1605
|
const stepDef = defSteps ? defSteps[currentIdx] : null
|
|
1587
1606
|
if (stepDef && !stepDef.optional) {
|
|
@@ -151,6 +151,6 @@ export function formatConcurrentWarning(detected) {
|
|
|
151
151
|
for (const c of otherActiveChanges) lines.push(` - ${c}`)
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
lines.push('提交请用显式 pathspec 隔离本变更文件,勿 git add .
|
|
154
|
+
lines.push('提交请用显式 pathspec 隔离本变更文件,勿 git add . 扫入他者工作(execute task 内建议 sillyspec wt-commit 串行提交)。')
|
|
155
155
|
return lines.join('\n')
|
|
156
156
|
}
|
package/src/run/gate-snapshot.js
CHANGED
|
@@ -34,6 +34,16 @@ export function createGateSnapshot({ cwd, files, sourceRoot = null }) {
|
|
|
34
34
|
// 前置:主仓须是 git 仓且有 HEAD(无 git 环境回退主仓现行为)
|
|
35
35
|
git(cwd, ['rev-parse', 'HEAD'])
|
|
36
36
|
|
|
37
|
+
// symlink-store 布局探测(坑 gate-snapshot-pnpm-store-break,2026-09-12 驾驭第十七批②,
|
|
38
|
+
// 用户第三次踩「lint 沙箱临时目录跑 pnpm 必假败」):pnpm/bun/lerna 的 node_modules 内部
|
|
39
|
+
// 是指向 store 的符号链接网——junction 进临时目录后跨根解析失效,实测必假败。
|
|
40
|
+
// 与其让沙箱报无关错误逼 advisory,不如布局命中即作废快照回退主仓(宁可主仓口径)。
|
|
41
|
+
const layoutHit = detectSymlinkStoreLayout(cwd)
|
|
42
|
+
if (layoutHit) {
|
|
43
|
+
console.warn(`⚠️ 门禁快照对 ${layoutHit} 布局不可靠(node_modules 符号链接 store 经 junction 跨根失效,实测必假败)——跳过快照回退主仓实测;主仓失败再做污染归属鉴定`)
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
|
|
37
47
|
snapshotRoot = mkdtempSync(join(tmpdir(), 'sillyspec-gate-'))
|
|
38
48
|
git(cwd, ['worktree', 'add', '--detach', '--quiet', snapshotRoot, 'HEAD'])
|
|
39
49
|
|
|
@@ -58,11 +68,39 @@ export function createGateSnapshot({ cwd, files, sourceRoot = null }) {
|
|
|
58
68
|
overlaid++
|
|
59
69
|
}
|
|
60
70
|
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
71
|
+
// 环境目录链接(坑 gate-snapshot-env-mismatch,2026-09-12 驾驭第十三批③,用户实证:
|
|
72
|
+
// 沙箱实测 venv 不装 dev 依赖(xdist 缺失)+ node_modules 缺失——快照只链 node_modules 时
|
|
73
|
+
// Python 项目的 commands.test 在快照内找不到 venv,误伤持续且难归因)。链接面扩 venv 族
|
|
74
|
+
// (.venv/venv/env——链接后 dev 依赖与主仓同源,不缺 xdist);链接失败/主仓本就没有时
|
|
75
|
+
// ⚠️ 显式可见(环境不一致的门禁会持续误伤——宁可吵不可静默错)。
|
|
76
|
+
// 递归依赖发现(坑 gate-snapshot-monorepo-layout,2026-09-12 驾驭第十五批①,用户实证
|
|
77
|
+
// 「对 monorepo 依赖布局完全不可用,四次重试才定位」):pnpm/nx/lerna workspace 的
|
|
78
|
+
// packages/<pkg>/node_modules 子目录依赖不在根四目录——深度≤3 扫描发现集逐个 junction。
|
|
79
|
+
const envRelDirs = discoverEnvDirs(cwd)
|
|
80
|
+
for (const rel of envRelDirs) {
|
|
81
|
+
const src = join(cwd, rel)
|
|
82
|
+
const dst = join(snapshotRoot, rel)
|
|
83
|
+
try {
|
|
84
|
+
mkdirSync(dirname(dst), { recursive: true })
|
|
85
|
+
symlinkSync(src, dst, 'junction')
|
|
86
|
+
} catch (e) {
|
|
87
|
+
console.warn(`⚠️ 门禁快照环境目录链接失败(${rel}):${e && e.message ? e.message : e}——快照内实测可能因缺该目录误伤,失败时先核对快照环境`)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (envRelDirs.length === 0) {
|
|
91
|
+
console.warn(`⚠️ 门禁快照:主仓未发现任何环境目录(node_modules / venv 族,含子包递归)——commands.test/lint 若依赖它们将快照/主仓都不可用(环境未安装?)`)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 环境完整性预检(坑 gate-snapshot-env-mismatch 二阶,2026-09-12 驾驭第十四批①,用户
|
|
95
|
+
// 实证「verify lint 沙箱必挂 node_modules 缺失只能 advisory」):主仓存在某环境目录而
|
|
96
|
+
// 快照内缺失(链接失败/布局差异)→ 快照对该仓 commands 是假环境,实测必挂——快照作废
|
|
97
|
+
// 回退主仓现行为(主仓口径可能被并行脏文件污染,但环境真实;两害取轻 + 回退原因可见)。
|
|
98
|
+
const envMissing = envDirsLinked(cwd, snapshotRoot)
|
|
99
|
+
if (envMissing.length > 0) {
|
|
100
|
+
console.warn(`⚠️ 门禁快照环境不完整(${envMissing.join('、')} 在主仓存在、快照内缺失)——快照作废回退主仓实测(宁可主仓口径不可假沙箱硬挂;回退后失败先做污染归属鉴定)`)
|
|
101
|
+
try { git(cwd, ['worktree', 'remove', '--force', '--quiet', snapshotRoot]) } catch {}
|
|
102
|
+
try { rmSync(snapshotRoot, { recursive: true, force: true }) } catch {}
|
|
103
|
+
return null
|
|
66
104
|
}
|
|
67
105
|
|
|
68
106
|
// local.yaml(gitignore 不进 HEAD;门禁命令配置来源)+ package-lock 保持 HEAD 版(npm test 不装新依赖)
|
|
@@ -90,6 +128,78 @@ export function createGateSnapshot({ cwd, files, sourceRoot = null }) {
|
|
|
90
128
|
}
|
|
91
129
|
}
|
|
92
130
|
|
|
131
|
+
|
|
132
|
+
/** symlink-store 包管理布局探测:命中即 junction 快照不可靠。返回布局名或 null。 */
|
|
133
|
+
export function detectSymlinkStoreLayout(cwd) {
|
|
134
|
+
try {
|
|
135
|
+
if (existsSync(join(cwd, 'pnpm-lock.yaml'))) return 'pnpm'
|
|
136
|
+
if (existsSync(join(cwd, 'bun.lockb')) || existsSync(join(cwd, 'bun.lock'))) return 'bun'
|
|
137
|
+
if (existsSync(join(cwd, 'lerna.json'))) return 'lerna'
|
|
138
|
+
try {
|
|
139
|
+
const pkg = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8'))
|
|
140
|
+
const pm = pkg && pkg.packageManager ? String(pkg.packageManager) : ''
|
|
141
|
+
if (/pnpm@/.test(pm)) return 'pnpm(packageManager)'
|
|
142
|
+
if (/yarn@/.test(pm)) return 'yarn(packageManager)'
|
|
143
|
+
if (/bun@/.test(pm)) return 'bun(packageManager)'
|
|
144
|
+
} catch { /* 无 package.json/损坏 → 仅锁文件判据 */ }
|
|
145
|
+
return null
|
|
146
|
+
} catch { return null }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** 环境目录名集(根与子包通用) */
|
|
150
|
+
const ENV_DIR_NAMES = new Set(['node_modules', '.venv', 'venv', 'env'])
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 递归依赖发现(坑 gate-snapshot-monorepo-layout):深度 ≤3 扫描(跳过环境目录自身内部/
|
|
154
|
+
* .git/dist/build/.sillyspec),返回所有环境目录的仓库根相对 POSIX 路径(含根级四目录)。
|
|
155
|
+
* pnpm/nx/lerna workspace 的 packages/<pkg>/node_modules 覆盖。
|
|
156
|
+
*/
|
|
157
|
+
export function discoverEnvDirs(cwd, { maxDepth = 3 } = {}) {
|
|
158
|
+
const out = []
|
|
159
|
+
const skip = new Set(['.git', 'dist', 'build', '.sillyspec', 'out', 'target'])
|
|
160
|
+
const walk = (dir, rel, depth) => {
|
|
161
|
+
let entries
|
|
162
|
+
try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return }
|
|
163
|
+
for (const e of entries) {
|
|
164
|
+
if (!e.isDirectory() || skip.has(e.name)) continue
|
|
165
|
+
const childRel = rel ? rel + '/' + e.name : e.name
|
|
166
|
+
if (ENV_DIR_NAMES.has(e.name)) {
|
|
167
|
+
out.push(childRel)
|
|
168
|
+
continue // 环境目录内部不再下钻(node_modules/node_modules 无意义且巨大)
|
|
169
|
+
}
|
|
170
|
+
if (depth < maxDepth) walk(join(dir, e.name), childRel, depth + 1)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
walk(cwd, '', 0)
|
|
174
|
+
return out
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* 环境完整性纯检(坑 gate-snapshot-env-mismatch 二阶,发现集口径):主仓存在(递归发现)
|
|
179
|
+
* 而快照缺失的环境目录清单。任一命中 = 快照对该仓 commands 是假环境,调用方作废回退主仓。
|
|
180
|
+
* @returns {string[]} 缺失目录相对路径(空数组 = 完整/主仓本就无环境目录)
|
|
181
|
+
*/
|
|
182
|
+
export function envDirsLinked(cwd, snapshotRoot) {
|
|
183
|
+
const missing = []
|
|
184
|
+
for (const rel of discoverEnvDirs(cwd)) {
|
|
185
|
+
try {
|
|
186
|
+
if (existsSync(join(cwd, rel)) && !existsSync(join(snapshotRoot, rel))) missing.push(rel)
|
|
187
|
+
} catch { missing.push(rel) /* 判定异常按缺失算(保守作废快照) */ }
|
|
188
|
+
}
|
|
189
|
+
return missing
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* 快照内实测失败的归属提示(坑 gate-snapshot-monorepo-layout 配套:用户四次重试才从日志
|
|
194
|
+
* 摸到 Temp\sillyspec-gate-* 路径——失败时路径/疑点/对照复跑出口必须直给)。
|
|
195
|
+
* 调用方在「快照内执行且失败」的分支打印。
|
|
196
|
+
*/
|
|
197
|
+
export function printSnapshotFailureHint(snapInfo, { offEnv = 'SILLYSPEC_VERIFY_GATE_SNAPSHOT_OFF' } = {}) {
|
|
198
|
+
if (!snapInfo || !snapInfo.snapshotRoot) return
|
|
199
|
+
console.error(` 🔬 本次实测执行于隔离快照:${snapInfo.snapshotRoot}(HEAD + 本变更 ${snapInfo.changeFileCount ?? '?'} 个文件${snapInfo.sourceRoot ? ',overlay 自 worktree' : ''})`)
|
|
200
|
+
console.error(` 失败疑点排查顺序:① 本变更文件自身的真实失败(最常见)→ 修代码;② 快照环境差异(monorepo 依赖布局/环境目录链接缺失)→ 设 ${offEnv}=1 回退主仓复跑对照——主仓过而快照挂即环境问题;③ 并行污染已被快照隔离,不在疑点内。`)
|
|
201
|
+
}
|
|
202
|
+
|
|
93
203
|
/**
|
|
94
204
|
* verify 门专用快照(2026-09-10 驾驭小结第八批,用户第二次真实阻塞:verify 实测门跑
|
|
95
205
|
* main 工作区,多会话并发任何人的 WIP 都能弄红别人的门)。等价「--worktree 定向跑」:
|
package/src/run/gates.js
CHANGED
|
@@ -485,8 +485,14 @@ export async function enforceReviewJsonGate(stageName, cwd, changeName, step, st
|
|
|
485
485
|
* 若校验失败不回滚,DB 会与真实产物不一致(hook/doctor/下游阶段全部误判),
|
|
486
486
|
* 且所有步骤都是 completed 时 agent 无法重新 --done("没有待完成的步骤")。
|
|
487
487
|
* 此处将 stage 回滚为 in-progress,最后一步重置为 pending,供修复产物后重做。
|
|
488
|
+
*
|
|
489
|
+
* 批量乐观标记一并回滚(坑 execute-batch-rollback-half-state,2026-08-29 用户实证):execute
|
|
490
|
+
* 批量完成(detectExecuteBatchFinish)把剩余步骤批量标 completed 后任一收尾 gate 失败,原先只
|
|
491
|
+
* 回滚 currentIdx → 落库「步骤 11 pending + 12-16 completed」半套状态,步骤序倒挂、要再补一次
|
|
492
|
+
* --done 才对齐。批量标记打 _batchAligned 戳(内存字段不落库),gate 失败 = 批量完成整体不成立,
|
|
493
|
+
* 戳上的步骤随当前步一起回滚 pending,下次 --done 满足条件再重新批量标记。
|
|
488
494
|
*/
|
|
489
|
-
function rollbackStageCompletion(stageData, steps, currentIdx) {
|
|
495
|
+
export function rollbackStageCompletion(stageData, steps, currentIdx) {
|
|
490
496
|
// 辅助阶段在 validator 前已被重置为 pending(steps 也换成了新数组),不要覆盖
|
|
491
497
|
if (stageData.status === 'completed') {
|
|
492
498
|
stageData.status = 'in-progress'
|
|
@@ -496,6 +502,14 @@ function rollbackStageCompletion(stageData, steps, currentIdx) {
|
|
|
496
502
|
steps[currentIdx].status = 'pending'
|
|
497
503
|
steps[currentIdx].completedAt = null
|
|
498
504
|
}
|
|
505
|
+
for (const s of steps) {
|
|
506
|
+
if (!s) continue
|
|
507
|
+
if (s._batchAligned && s.status === 'completed') {
|
|
508
|
+
s.status = 'pending'
|
|
509
|
+
s.completedAt = null
|
|
510
|
+
}
|
|
511
|
+
s._batchAligned = false
|
|
512
|
+
}
|
|
499
513
|
}
|
|
500
514
|
|
|
501
515
|
/**
|
|
@@ -511,7 +525,7 @@ function rollbackCompletionAndReturn(pm, progress, stageData, steps, currentIdx,
|
|
|
511
525
|
// friction 尾参(friction-signal-hint task-02):gate 失败摩擦埋点标签 {type, detail}——各调用点
|
|
512
526
|
// 按所属 gate 段传来源标签(审查两段走 review_rejected),未传兜底通用 gate-cascade。
|
|
513
527
|
// recordFrictionEvent 自带静默降级(非法类型/写失败返回 null),不影响回滚/返回语义。
|
|
514
|
-
recordFrictionEvent({ cwd, changeName, platformOpts, ...(friction || { type: 'gate_rollback', detail: 'gate-cascade' }) })
|
|
528
|
+
void recordFrictionEvent({ cwd, changeName, platformOpts, ...(friction || { type: 'gate_rollback', detail: 'gate-cascade' }) })
|
|
515
529
|
rollbackStageCompletion(stageData, steps, currentIdx)
|
|
516
530
|
progress.lastActive = new Date().toLocaleString('zh-CN', { hour12: false })
|
|
517
531
|
pm._write(cwd, progress, changeName)
|
|
@@ -646,7 +660,7 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
646
660
|
const { createVerifyGateSnapshot } = await import('./gate-snapshot.js')
|
|
647
661
|
verifyGateSnap = await createVerifyGateSnapshot({ cwd, changeName, specBase, platformOpts })
|
|
648
662
|
if (verifyGateSnap) {
|
|
649
|
-
console.log(`🧪 Verify
|
|
663
|
+
console.log(`🧪 Verify 实测隔离快照(根:${verifyGateSnap.snapshotRoot}):HEAD + 本变更 ${verifyGateSnap.changeFileCount} 个文件${verifyGateSnap.sourceRoot ? '(overlay 自 worktree——本变更分支内容定向跑)' : ''}(并行会话脏文件不参与门判定)`)
|
|
650
664
|
}
|
|
651
665
|
} catch { /* 快照链路异常 → 主仓现行为 */ }
|
|
652
666
|
}
|
|
@@ -679,7 +693,11 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
679
693
|
}
|
|
680
694
|
} catch { /* fail-soft:回填失败不影响门禁 */ }
|
|
681
695
|
if (testCheck.status === 'failed') {
|
|
682
|
-
if (verifyGateSnap) {
|
|
696
|
+
if (verifyGateSnap) {
|
|
697
|
+
try { const { printSnapshotFailureHint } = await import('./gate-snapshot.js')
|
|
698
|
+
printSnapshotFailureHint(verifyGateSnap) } catch { /* 提示失败不影响阻断 */ }
|
|
699
|
+
try { verifyGateSnap.cleanup() } catch {} ; verifyGateSnap = null
|
|
700
|
+
}
|
|
683
701
|
console.error('\n❌ verify 阶段被阻断:verify-result.md 自报告通过,但 CLI 实测测试失败。')
|
|
684
702
|
// 并行 WIP 归因鉴别(坑 verify-reconcile-foreign-wip):单一实现迁至 verify-quality-scan.js
|
|
685
703
|
// renderVerifyTestAttribution(P0-1 前置二:noAI 质量扫描失败路径与本 --done 路径共用,
|
|
@@ -700,13 +718,21 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
700
718
|
if (lintCheck) {
|
|
701
719
|
console.log(`\n♻️ Verify lint 对账:复用 noAI 质量扫描步的实测结果(代码指纹匹配,免重跑)`)
|
|
702
720
|
} else {
|
|
703
|
-
lintCheck = runVerifyLintCheck({ cwd: gateCwd, specBase: gateSpecBase })
|
|
721
|
+
lintCheck = runVerifyLintCheck({ cwd: gateCwd, specBase: gateSpecBase, timeoutMs: verifyGateSnap ? 5 * 60 * 1000 : undefined })
|
|
722
|
+
// 快照 lint 超时回退主仓(2026-09-12 dogfood 两连实证:junction I/O 病态慢,3min/5min 均被
|
|
723
|
+
// 超时杀——主仓 60~110s 正常。按用户建议「自动回退」而非假败/advisory:主仓复跑保硬门,
|
|
724
|
+
// 并行噪声混入时失败输出带归属鉴定提示)
|
|
725
|
+
if (verifyGateSnap && lintCheck.status === 'failed' && /超时/.test(String(lintCheck.reason || ''))) {
|
|
726
|
+
console.warn('⚠️ 快照 lint 超时(node_modules junction I/O 慢)→ 主仓复跑 lint(并行噪声可能混入——失败先做归属鉴定)')
|
|
727
|
+
lintCheck = runVerifyLintCheck({ cwd, specBase })
|
|
728
|
+
}
|
|
704
729
|
if (lintCheck.status !== 'skipped') {
|
|
705
730
|
console.log(`\n⏳ Verify lint 对账:CLI 亲自执行 local.yaml 的 commands.lint…`)
|
|
706
731
|
}
|
|
707
732
|
}
|
|
708
733
|
// 快照收尾(test+lint 两检查都已完成;parity/删除探针走主仓 git 事实不依赖快照)
|
|
709
734
|
const usedSnapshot = Boolean(verifyGateSnap)
|
|
735
|
+
const usedSnapInfo = verifyGateSnap ? { snapshotRoot: verifyGateSnap.snapshotRoot, changeFileCount: verifyGateSnap.changeFileCount, sourceRoot: verifyGateSnap.sourceRoot } : null
|
|
710
736
|
if (verifyGateSnap) { try { verifyGateSnap.cleanup() } catch {} ; verifyGateSnap = null }
|
|
711
737
|
printVerifyLintCheck(lintCheck)
|
|
712
738
|
// lint 硬门(2026-09-09 升硬:观察期 14 次 5 败全真阳性,pre-push 本就硬拦——fail-fast 前移;
|
|
@@ -714,6 +740,10 @@ export async function runStageCompletionGates({ stageName, cwd, changeName, plat
|
|
|
714
740
|
const { shouldBlockVerifyLint } = await import('../verify-postcheck.js')
|
|
715
741
|
if (shouldBlockVerifyLint(lintCheck)) {
|
|
716
742
|
console.error('\n❌ verify 阶段被阻断:CLI 亲自实测 lint 失败(agent 的 lint 自报告与实测不符时以实测为准)。')
|
|
743
|
+
if (usedSnapInfo) {
|
|
744
|
+
try { const { printSnapshotFailureHint } = await import('./gate-snapshot.js')
|
|
745
|
+
printSnapshotFailureHint(usedSnapInfo) } catch { /* 提示失败不影响阻断 */ }
|
|
746
|
+
}
|
|
717
747
|
if (lintCheck.outputTail) {
|
|
718
748
|
const tail = lintCheck.outputTail.split('\n').slice(-8).join('\n')
|
|
719
749
|
console.error(' 输出(末尾):')
|
package/src/run/prompt.js
CHANGED
|
@@ -22,7 +22,7 @@ import { basename, join } from 'node:path'
|
|
|
22
22
|
import { existsSync, readFileSync, mkdirSync, readdirSync } from 'node:fs'
|
|
23
23
|
import { writeAtomicSync } from '../fs-atomic.js'
|
|
24
24
|
import { stageRegistry } from '../stages/index.js'
|
|
25
|
-
import { resolvePromptIncludes, resolveRuntimeRoot, safeGit, parsePorcelainPath, WAIT_MARKER_RE, QUICK_SID_RE } from './shared.js'
|
|
25
|
+
import { resolvePromptIncludes, resolveRuntimeRoot, safeGit, parsePorcelainPath, WAIT_MARKER_RE, QUICK_SID_RE, triggerStepStartSync } from './shared.js'
|
|
26
26
|
import { renderSemanticGuardBlock, readSemanticGuardEnabled } from '../semantic-guard.js'
|
|
27
27
|
import { renderStageContract } from '../stage-contract-spec.js'
|
|
28
28
|
import { nowWallClock } from '../datetime.js'
|
|
@@ -309,6 +309,14 @@ export async function outputStep(stageName, stepIndex, steps, cwd, changeName, d
|
|
|
309
309
|
return false
|
|
310
310
|
}
|
|
311
311
|
|
|
312
|
+
// X3 渲染侧接线(2026-08-29-change-delete-closure-and-spec-pull 遗留活跃坑收口;曾因并行
|
|
313
|
+
// 会话提交丢弃一次,2026-09-12 复检重接):步骤 prompt 渲染即步骤起点——补推一次 progress
|
|
314
|
+
// 刷新平台 last_pushed_at(停滞判定不再把长步骤误报为「停滞」)。不 await:prompt 渲染不被
|
|
315
|
+
// 网络阻塞;未连接平台静默 / quick 会话降级跳过 / 8s 熔断 / 失败只 warn,契约全在
|
|
316
|
+
// triggerStepStartSync 内(shared.js docstring 即本接线点的说明),Node 事件循环保证在飞
|
|
317
|
+
// POST 落地后进程才退出。
|
|
318
|
+
triggerStepStartSync(cwd, changeName, platformOpts)
|
|
319
|
+
|
|
312
320
|
// ── 首个 agent 可见步索引(「首步一次建立」类注入的锚点)──
|
|
313
321
|
// step0 为 noAI(brainstorm/execute/verify 的 step1「进度确认」,2026-09-07)时不渲染 prompt,
|
|
314
322
|
// persona/护栏/完成契约/铁律/平台路径约束等 step0 专属注入须顺延到首个渲染步,否则整段丢失
|
package/src/run/quick-audit.js
CHANGED
|
@@ -137,6 +137,17 @@ export function printQuickAuditReview(review) {
|
|
|
137
137
|
}
|
|
138
138
|
console.warn(` quick 不强制文档同步,欠账已随本条 QUICKLOG「审计:」行落盘(可事后追溯)。若改动触及接口/契约,建议顺手同步模块文档。`)
|
|
139
139
|
}
|
|
140
|
+
// spec 共享面他者噪声(坑 foreign-spec-churn-fail-closed):并行会话归档移动/文档收尾的
|
|
141
|
+
// 删除与 EOL-only 假 M——软警告点名归因,不再逼 --allow-delete/--allow-new 解锁。
|
|
142
|
+
if (review.foreignSpecChurn && review.foreignSpecChurn.length > 0) {
|
|
143
|
+
console.warn(`⚠️ spec 共享面 ${review.foreignSpecChurn.length} 个非本会话声明的删除(.sillyspec/ docs/,疑似并行会话归档移动/收尾——归其会话收口,本会话不阻断):`)
|
|
144
|
+
for (const f2 of review.foreignSpecChurn.slice(0, 6)) console.warn(` - ${f2}`)
|
|
145
|
+
if (review.foreignSpecChurn.length > 6) console.warn(` … 共 ${review.foreignSpecChurn.length} 个`)
|
|
146
|
+
}
|
|
147
|
+
if (review.eolOnlyFiles && review.eolOnlyFiles.length > 0) {
|
|
148
|
+
console.warn(`⚠️ ${review.eolOnlyFiles.length} 个文件仅行尾重写(内容零变化,gen:types/编辑器 CRLF 假 M——已剔出本会话审计):${review.eolOnlyFiles.slice(0, 5).join('、')}${review.eolOnlyFiles.length > 5 ? ' 等' : ''}`)
|
|
149
|
+
console.warn(` 根治:.gitattributes 声明 text=auto eol=lf(或按文件类型钉死),生成器重写不再制造假 M`)
|
|
150
|
+
}
|
|
140
151
|
if (review.docsCheckHint && review.docsCheckHint.invalid > 0) {
|
|
141
152
|
console.warn(`\n📎 文档引用失效(docs check):本次改动的文档含 ${review.docsCheckHint.invalid}/${review.docsCheckHint.total} 处失效 file:line 引用。`)
|
|
142
153
|
// 逐条指名(2026-09-11 用户实证:只报计数不指名哪处,四轮复现才定位——门禁输出
|
|
@@ -338,16 +349,28 @@ export async function runQuickTestLintGate({ cwd, specBase, changedFiles = [], d
|
|
|
338
349
|
if (snapshot) {
|
|
339
350
|
gateCwd = snapshot.snapshotRoot
|
|
340
351
|
gateSpecBase = join(snapshot.snapshotRoot, '.sillyspec')
|
|
341
|
-
console.log(`🧪
|
|
352
|
+
console.log(`🧪 门禁隔离快照(根:${snapshot.snapshotRoot}):HEAD + 本会话 ${snapshot.overlaid} 个文件(并行会话脏文件不参与判定)`)
|
|
342
353
|
}
|
|
343
354
|
} catch { /* 快照链路异常 → 主仓现行为 */ }
|
|
344
355
|
}
|
|
345
356
|
try {
|
|
346
357
|
const test = runVerifyTestCheck({ cwd: gateCwd, specBase: gateSpecBase, changeName })
|
|
347
|
-
|
|
358
|
+
let lint = runVerifyLintCheck({ cwd: gateCwd, specBase: gateSpecBase, timeoutMs: snapshot ? 5 * 60 * 1000 : undefined })
|
|
359
|
+
// 快照 lint 超时回退主仓(2026-09-12 dogfood 两连实证:junction I/O 病态慢,3min/5min 均被
|
|
360
|
+
// 超时杀——主仓 60~110s 正常。按用户建议「自动回退」而非假败/advisory:主仓复跑保硬门,
|
|
361
|
+
// 并行噪声混入时失败输出带归属鉴定提示)
|
|
362
|
+
if (snapshot && lint.status === 'failed' && /超时/.test(String(lint.reason || ''))) {
|
|
363
|
+
console.warn('⚠️ 快照 lint 超时(node_modules junction I/O 慢)→ 主仓复跑 lint(并行噪声可能混入——失败先做归属鉴定)')
|
|
364
|
+
lint = runVerifyLintCheck({ cwd, specBase })
|
|
365
|
+
}
|
|
348
366
|
const failed = []
|
|
349
367
|
if (test.status === 'failed') failed.push('test')
|
|
350
368
|
if (lint.status === 'failed') failed.push('lint')
|
|
369
|
+
if (failed.length > 0 && snapshot) {
|
|
370
|
+
try { const { printSnapshotFailureHint } = await import('./gate-snapshot.js')
|
|
371
|
+
printSnapshotFailureHint({ snapshotRoot: snapshot.snapshotRoot, changeFileCount: snapshot.overlaid, sourceRoot: snapshot.sourceRoot || null }, { offEnv: 'SILLYSPEC_QUICK_GATE_SNAPSHOT_OFF' })
|
|
372
|
+
} catch { /* 提示失败不影响门禁语义 */ }
|
|
373
|
+
}
|
|
351
374
|
return {
|
|
352
375
|
action: failed.length > 0 ? 'fail' : 'pass',
|
|
353
376
|
failed,
|