sillyspec 3.28.7 → 3.28.9
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-quick/SKILL.md +2 -0
- package/.husky/pre-push +3 -1
- package/AGENTS.md +3 -2
- package/README.md +35 -11
- package/SKILL.md +24 -10
- package/SillySpec-/350/203/275/345/212/233/344/272/256/347/202/271/345/205/250/346/231/257-2026-09-14.pptx +0 -0
- package/package.json +1 -1
- package/src/agent-session-log.js +446 -35
- package/src/change-risk-profile.js +20 -0
- package/src/config-cat.js +6 -3
- package/src/config-schema.js +32 -0
- package/src/constants.js +3 -0
- package/src/db.js +23 -6
- package/src/decision-distill.js +15 -3
- package/src/design-facts.js +33 -3
- package/src/docs-check.js +137 -12
- package/src/docs-gate.js +108 -6
- package/src/doctor-diagnostics.js +243 -9
- package/src/friction-ledger.js +171 -0
- package/src/friction-tally.js +7 -2
- package/src/git-helper.js +6 -4
- package/src/hooks/worktree-guard.js +72 -30
- package/src/index.js +356 -81
- package/src/knowledge-classify.js +363 -0
- package/src/knowledge-hits.js +74 -0
- package/src/knowledge-stats.js +180 -0
- 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 +224 -43
- package/src/progress/consistency-doctor.js +11 -4
- package/src/progress/shared.js +3 -2
- package/src/progress/stage-machine.js +3 -1
- package/src/progress/step-store.js +30 -17
- package/src/progress.js +123 -8
- package/src/quick-gate-profile.js +251 -0
- package/src/quicklog.js +199 -32
- package/src/review-dispatch.js +2 -1
- package/src/run/command.js +124 -15
- package/src/run/complete-handlers.js +514 -24
- package/src/run/complete.js +82 -25
- package/src/run/concurrent-detect.js +99 -1
- package/src/run/gate-snapshot.js +115 -5
- package/src/run/gates.js +85 -14
- package/src/run/prompt.js +143 -6
- package/src/run/quick-audit.js +136 -15
- package/src/run/shared.js +174 -20
- package/src/run/stage.js +6 -0
- package/src/run/verify-quality-scan.js +14 -3
- package/src/scan-diff.js +61 -30
- package/src/scan-postcheck.js +74 -0
- package/src/scan-refresh.js +477 -0
- package/src/scan-staleness.js +25 -13
- package/src/scope-audit.js +365 -35
- package/src/spec-sync.js +19 -14
- package/src/stage-contract-spec.js +4 -3
- package/src/stage-contract.js +185 -2
- package/src/stage-review.js +29 -12
- package/src/stages/brainstorm.js +7 -1
- package/src/stages/execute.js +91 -4
- package/src/stages/knowledge.js +52 -3
- package/src/stages/plan-postcheck.js +57 -12
- package/src/stages/quick.js +1 -1
- package/src/stages/verify.js +1 -0
- package/src/sync-noise.js +114 -6
- package/src/sync.js +201 -11
- package/src/task-review.js +333 -27
- package/src/verify-facts-schema.js +10 -3
- package/src/verify-postcheck.js +41 -6
- package/src/verify-probes.js +348 -7
- package/src/worktree-apply.js +594 -62
- package/src/worktree-cross.js +12 -18
- package/src/worktree-deps.js +100 -2
- package/src/worktree.js +255 -30
- package/src/wt-commit.js +128 -0
- package/templates/agents-instruction.md +2 -2
- package/templates/prompts/testcase-design.md +2 -1
- package/templates/prompts/verify-probes.md +4 -1
- package/sa-rename-probe/src/new.js +0 -4
package/src/run/complete.js
CHANGED
|
@@ -23,6 +23,7 @@ import { writeAtomicSync } from '../fs-atomic.js'
|
|
|
23
23
|
import { gitQuiet } from '../git-helper.js'
|
|
24
24
|
import { withFileLock } from '../quicklog.js'
|
|
25
25
|
import { triggerSync, WAIT_MARKER_RE, getStageSteps, formatWaitOptions, resolveRuntimeRoot, getOrCreateMultiRepoContext, resolveChangeDir } from './shared.js'
|
|
26
|
+
import { isExplicitReviewWrite } from '../task-review.js'
|
|
26
27
|
|
|
27
28
|
// auto 模式 brainstorm 步骤表感知解析(2026-09-08 E2E 实证 bug:--done/--wait 推进主模式 8 步表,
|
|
28
29
|
// 与 runAutoMode getAutoSteps 的 4 步 auto 表双轨互踩——ensureAutoStage 判非 auto 表重种清零,
|
|
@@ -119,7 +120,7 @@ export function synthesizeStepOutput({ stageName, stepName, cwd }) {
|
|
|
119
120
|
}
|
|
120
121
|
|
|
121
122
|
export async function completeStep(pm, progress, stageName, cwd, outputText, inputText = null, options = {}) {
|
|
122
|
-
const { printNext = true, confirm = false, changeName, platformOpts = {}, nonInteractive = false, isForceBaseline = false, isAllowNew = false, isAllowDelete = false, quickFiles = [] } = options
|
|
123
|
+
const { printNext = true, confirm = false, changeName, platformOpts = {}, nonInteractive = false, isForceBaseline = false, isAllowNew = false, isAllowDelete = false, isNoDocs = false, isSkipApply = false, sessionFlag = null, quickFiles = [] } = options
|
|
123
124
|
// specRoot(平台) > specDriftAnchor(worktree 漂移锚定主仓) > cwd/.sillyspec(本地)——
|
|
124
125
|
// 与 prompt.js resolvePromptSpecBase 同序:--done 收尾的 user-inputs.md/超长 artifact 落盘
|
|
125
126
|
// 必须与 prompt 渲染同一根,否则写进 worktree 副本目录、随 cleanup 整目录删除。
|
|
@@ -494,7 +495,7 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
494
495
|
// archive「确认归档」收尾:--confirm 门控 + archiveChangeDirectory 移动 + 推荐文档校验
|
|
495
496
|
// (抽成 handleArchiveConfirmStep;缺 --confirm 时返回 early-return 对象,completeStep 透传)
|
|
496
497
|
{
|
|
497
|
-
const _archiveEarlyReturn = await handleArchiveConfirmStep({ stageName, steps, currentIdx, confirm, outputText, pm, cwd, progress, changeName, specBase, platformOpts })
|
|
498
|
+
const _archiveEarlyReturn = await handleArchiveConfirmStep({ stageName, steps, currentIdx, confirm, outputText, pm, cwd, progress, changeName, specBase, platformOpts, isSkipApply, sessionFlag })
|
|
498
499
|
if (_archiveEarlyReturn) return _archiveEarlyReturn
|
|
499
500
|
}
|
|
500
501
|
|
|
@@ -568,8 +569,10 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
568
569
|
console.log(` 恢复:sillyspec run ${stageName} --continue --answer "用户回答"${changeName ? ` --change ${changeName}` : ''}`)
|
|
569
570
|
return { stageCompleted: false, currentIdx, nextPendingIdx: -1 }
|
|
570
571
|
}
|
|
571
|
-
// quick 收尾(W6 Step6b 抽至 complete-handlers.js handleQuickStageCompletion
|
|
572
|
-
|
|
572
|
+
// quick 收尾(W6 Step6b 抽至 complete-handlers.js handleQuickStageCompletion);
|
|
573
|
+
// isNoDocs(task-02)--no-docs 豁免随链透传进 auditQuickCompletion options;
|
|
574
|
+
// sessionFlag(task-03)--session 随链透传进 quick 轻量归档链所有权校验
|
|
575
|
+
await handleQuickStageCompletion({ stageName, steps, currentIdx, cwd, progress, changeName, specBase, outputText, confirm, isForceBaseline, isAllowNew, isAllowDelete, isNoDocs, sessionFlag, platformOpts, pm, quickFiles })
|
|
573
576
|
|
|
574
577
|
// ── reopen --done 回填(坑 brainstorm-reopen-step-state-desync)──
|
|
575
578
|
// nextPendingIdx === -1 且无 waiting,说明要进阶段完成分支。此时若存在 stale 步骤
|
|
@@ -673,9 +676,18 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
673
676
|
if (stageName === 'verify') {
|
|
674
677
|
try {
|
|
675
678
|
const { consumeFrictionHint } = await import('../friction-tally.js')
|
|
676
|
-
const r = consumeFrictionHint({ cwd, changeName, platformOpts })
|
|
679
|
+
const r = await consumeFrictionHint({ cwd, changeName, platformOpts })
|
|
677
680
|
if (r && r.hint) console.log(`\n${r.hint}`)
|
|
678
|
-
|
|
681
|
+
// 台账滚动(2026-09-15-tax-governance FR-02 主滚动点):consume 得到的非零 counts 按
|
|
682
|
+
// change 滚动合并进 <runtimeRoot>/friction-ledger.json 幸存台账(merge-by-change——
|
|
683
|
+
// --reopen 重跑二次 consume 累加非双计);空 counts 跳过写(防干净收尾落空文件);
|
|
684
|
+
// archivedAt 此时不落(prune 侧落定);runtimeRoot 与 consume 同源(specBase 同变量
|
|
685
|
+
// + resolveRuntimeRoot 单点)。台账写失败 fail-soft 在既有 try/catch 内,绝不阻断收尾。
|
|
686
|
+
if (changeName && r && r.counts && Object.keys(r.counts).length > 0) {
|
|
687
|
+
const { mergeFrictionEntry } = await import('../friction-ledger.js')
|
|
688
|
+
await mergeFrictionEntry(resolveRuntimeRoot(platformOpts, specBase), { change: changeName, counts: r.counts })
|
|
689
|
+
}
|
|
690
|
+
} catch { /* 摩擦提示/台账失败不影响收尾 */ }
|
|
679
691
|
}
|
|
680
692
|
|
|
681
693
|
if (stageName === 'execute') {
|
|
@@ -834,7 +846,7 @@ async function printExecuteScopeAudit({ cwd, changeName, specBase, platformOpts,
|
|
|
834
846
|
if (typeof frozenPatch === 'string') {
|
|
835
847
|
const patchText = frozenPatch.endsWith('\n') ? frozenPatch : frozenPatch + '\n'
|
|
836
848
|
writeFileSync(join(changeDir, 'scope-audit.patch'), patchText)
|
|
837
|
-
snap.patchSha256 = createHash('sha256').update(patchText, 'utf8').digest('hex')
|
|
849
|
+
snap.patchSha256 = createHash('sha256').update(patchText.replace(/\r\n/g, '\n'), 'utf8').digest('hex')
|
|
838
850
|
snap.patchStatus = 'ok'
|
|
839
851
|
} else if (frozenPatch === null) {
|
|
840
852
|
snap.patchStatus = 'failed'
|
|
@@ -975,35 +987,49 @@ function prefetchDiffFileSet(ctx) {
|
|
|
975
987
|
* changedFiles 非空且 git diff 实测非空。防 allowed_paths 误归属他人 diff 或陈旧 review.json 导致
|
|
976
988
|
* 空任务被自动勾选/批量放行。ctx 缺省时保持现行判定(向后兼容)。
|
|
977
989
|
*
|
|
978
|
-
*
|
|
990
|
+
* ql-20260915-001 修复②(坑 review-write-draft-marker-stuck,2026-09-14 用户实证:文档面任务
|
|
991
|
+
* 反复「草稿 changedFiles 为空,跳过自动勾选」断链):显式 CLI 写入(writeTaskReview 通道,
|
|
992
|
+
* writtenBy 锚定,带 agent 供给 verdict)一律按非草稿处理——不受 changedFiles 空/实测 diff 空
|
|
993
|
+
* 限制(.sillyspec/ 文档面任务归因被滤、纯验证任务零 diff 是合法形态;agent 已用
|
|
994
|
+
* review write --force --changed-files 显式表态,verdict 即结论)。草稿守卫警告同步改为
|
|
995
|
+
* 可行动:含 task 名与解锁命令示例,不再只报现象。
|
|
996
|
+
*
|
|
997
|
+
* @param {{ok?:boolean, review?:{specVerdict?:string, qualityVerdict?:string, reviewerNotes?:string, changedFiles?:string[], task?:string, writtenBy?:string}}} r readReview 结果
|
|
979
998
|
* @param {boolean} endToEnd 是否端到端/deployment-critical task
|
|
980
999
|
* @param {{gitDir?:string, base?:string, head?:string}|null} [ctx] 实测 diff 上下文;缺省保持现行判定
|
|
1000
|
+
* @param {string} [changeName] 变更名(草稿守卫警告里解锁命令示例用)
|
|
981
1001
|
* @returns {boolean}
|
|
982
1002
|
*/
|
|
983
|
-
export function shouldAutoCheckTask(r, endToEnd, ctx = null) {
|
|
1003
|
+
export function shouldAutoCheckTask(r, endToEnd, ctx = null, changeName = '') {
|
|
984
1004
|
if (!r?.ok) return false
|
|
985
1005
|
const spec = r.review?.specVerdict
|
|
986
1006
|
const quality = r.review?.qualityVerdict
|
|
987
1007
|
if (spec === 'fail' || quality === 'fail') return false
|
|
988
1008
|
if (endToEnd) return spec === 'pass' && quality === 'pass'
|
|
989
1009
|
|
|
1010
|
+
// ql-20260915-001 修复②:显式 CLI 写入(review write 通道)按非草稿处理,直接勾——
|
|
1011
|
+
// verdict 非 fail 已过上面守卫,文档面/纯验证任务的空 changedFiles 不再拦截
|
|
1012
|
+
if (isExplicitReviewWrite(r.review)) return true
|
|
1013
|
+
|
|
990
1014
|
// W2 task-04 草稿零 diff 守卫(FR-03)
|
|
991
1015
|
// ctx 给定且 review 为自动草稿时,要求 changedFiles 非空且实测 diff 非空
|
|
992
1016
|
if (ctx && r.review?.reviewerNotes && r.review.reviewerNotes.includes('auto-generated draft')) {
|
|
1017
|
+
const taskId = r.review.task || 'task-NN'
|
|
1018
|
+
const unlockCmd = `sillyspec review write${changeName ? ` --change ${changeName}` : ' --change <变更名>'} --task ${taskId} --spec pass --quality pass --force --changed-files <文件>`
|
|
993
1019
|
const changedFiles = r.review.changedFiles || []
|
|
994
1020
|
if (changedFiles.length === 0) {
|
|
995
|
-
console.warn(`⚠️ 草稿 review changedFiles
|
|
1021
|
+
console.warn(`⚠️ 草稿 review changedFiles 为空,跳过自动勾选(task=${taskId})——解锁:${unlockCmd}(纯验证任务用 --changed-files "" + --evidence "<验证说明>")`)
|
|
996
1022
|
return false
|
|
997
1023
|
}
|
|
998
1024
|
if (!ctx.gitDir || !ctx.base || !ctx.head) {
|
|
999
|
-
console.warn(`⚠️ ctx 信息不完整(gitDir=${ctx.gitDir}, base=${ctx.base}, head=${ctx.head}),跳过草稿 diff
|
|
1025
|
+
console.warn(`⚠️ ctx 信息不完整(gitDir=${ctx.gitDir}, base=${ctx.base}, head=${ctx.head}),跳过草稿 diff 校验,保守不勾(task=${taskId})——解锁:${unlockCmd}`)
|
|
1000
1026
|
return false
|
|
1001
1027
|
}
|
|
1002
1028
|
// 预取路径:一次全量 diff 文件集内存归属(autoCheckPlanFromReviews/批量路径已 prefetch)
|
|
1003
1029
|
if (ctx.diffFileSet) {
|
|
1004
1030
|
const hit = changedFiles.filter(f => ctx.diffFileSet.has(f))
|
|
1005
1031
|
if (hit.length === 0) {
|
|
1006
|
-
console.warn(`⚠️ 草稿 review 实测 diff 为空(base=${ctx.base.slice(0, 8)}, head=${ctx.head.slice(0, 8)}, files=${changedFiles.length}
|
|
1032
|
+
console.warn(`⚠️ 草稿 review 实测 diff 为空(task=${taskId}, base=${ctx.base.slice(0, 8)}, head=${ctx.head.slice(0, 8)}, files=${changedFiles.length}),跳过自动勾选——解锁:${unlockCmd}`)
|
|
1007
1033
|
return false
|
|
1008
1034
|
}
|
|
1009
1035
|
console.log(` ✓ 草稿 diff 校验通过(实测 ${hit.length}/${changedFiles.length} 个声明文件有改动)`)
|
|
@@ -1014,12 +1040,12 @@ export function shouldAutoCheckTask(r, endToEnd, ctx = null) {
|
|
|
1014
1040
|
// 用数组参数避免 shell 拆词,复用 git-helper 安全模式
|
|
1015
1041
|
const diffResult = gitQuiet(ctx.gitDir, ['diff', '--name-only', `${ctx.base}..${ctx.head}`, '--', ...changedFiles], { trim: true })
|
|
1016
1042
|
if (!diffResult || diffResult.trim() === '') {
|
|
1017
|
-
console.warn(`⚠️ 草稿 review 实测 diff 为空(base=${ctx.base.slice(0,8)}, head=${ctx.head.slice(0,8)}, files=${changedFiles.length}
|
|
1043
|
+
console.warn(`⚠️ 草稿 review 实测 diff 为空(task=${taskId}, base=${ctx.base.slice(0,8)}, head=${ctx.head.slice(0,8)}, files=${changedFiles.length}),跳过自动勾选——解锁:${unlockCmd}`)
|
|
1018
1044
|
return false
|
|
1019
1045
|
}
|
|
1020
1046
|
console.log(` ✓ 草稿 diff 校验通过(${diffResult.trim().split('\n').length} 个文件有改动)`)
|
|
1021
1047
|
} catch (e) {
|
|
1022
|
-
console.warn(`⚠️ 草稿 diff
|
|
1048
|
+
console.warn(`⚠️ 草稿 diff 校验失败(task=${taskId}: ${e && e.message ? e.message : e}),保守不勾——解锁:${unlockCmd}`)
|
|
1023
1049
|
return false
|
|
1024
1050
|
}
|
|
1025
1051
|
}
|
|
@@ -1071,9 +1097,15 @@ export async function autoCheckPlanFromReviews({ stageName, changeName, cwd, pla
|
|
|
1071
1097
|
const taskNum = match.match(/task-(\d+)/)[1].padStart(2, '0')
|
|
1072
1098
|
const reviewPath = join(runtimeRoot, 'execute-runs', executeRunId, 'tasks', `task-${taskNum}`, 'review.json')
|
|
1073
1099
|
const r = readReview(reviewPath)
|
|
1074
|
-
//
|
|
1075
|
-
|
|
1076
|
-
|
|
1100
|
+
// 卡片读取延后到 verdict 可用之后(2026-09-11 审查性能包③):review 缺失或 spec/quality
|
|
1101
|
+
// 已 fail 时 endToEnd 取值不影响 shouldAutoCheckTask 结果(恒 false),先读卡片是纯浪费
|
|
1102
|
+
// ——持 tasks 锁期间每 task 省一次 readFileSync。
|
|
1103
|
+
const verdictUsable = r?.ok
|
|
1104
|
+
&& r.review?.specVerdict !== 'fail'
|
|
1105
|
+
&& r.review?.qualityVerdict !== 'fail'
|
|
1106
|
+
const endToEnd = verdictUsable
|
|
1107
|
+
&& isEndToEndTaskText(match + ' ' + readTaskCardText(changeDir, taskNum))
|
|
1108
|
+
if (shouldAutoCheckTask(r, endToEnd, ctx, changeName)) {
|
|
1077
1109
|
checkedCount++
|
|
1078
1110
|
return `${p1}x${p2}` // 勾选
|
|
1079
1111
|
}
|
|
@@ -1169,7 +1201,10 @@ async function detectExecuteBatchFinish({ pm, stageName, changeName, cwd, specBa
|
|
|
1169
1201
|
}
|
|
1170
1202
|
|
|
1171
1203
|
// review 存在:判定是否为自动草稿且零 diff
|
|
1172
|
-
|
|
1204
|
+
// (ql-20260915-001 修复②:显式 CLI 写入(writeTaskReview 通道)按非草稿处理——不阻断
|
|
1205
|
+
// 批量,与勾选层 shouldAutoCheckTask 同口径;agent 已显式给 verdict 的 review 不该因
|
|
1206
|
+
// notes 字面残留 'auto-generated draft' 被当草稿拦截)
|
|
1207
|
+
if (!isExplicitReviewWrite(review) && review?.reviewerNotes && review.reviewerNotes.includes('auto-generated draft')) {
|
|
1173
1208
|
const changedFiles = review.changedFiles || []
|
|
1174
1209
|
// 草稿且 changedFiles 为空 → 阻断
|
|
1175
1210
|
if (changedFiles.length === 0) {
|
|
@@ -1212,6 +1247,11 @@ async function detectExecuteBatchFinish({ pm, stageName, changeName, cwd, specBa
|
|
|
1212
1247
|
if (step.status === 'pending' || step.status === 'in-progress') {
|
|
1213
1248
|
step.status = 'completed'
|
|
1214
1249
|
step.completedAt = now
|
|
1250
|
+
// 乐观预标戳:这是本次 --done 的批量乐观标记而非逐步验证的完成。任一收尾 gate 失败时
|
|
1251
|
+
// rollbackStageCompletion 据此一并回滚(坑 execute-batch-rollback-half-state:只回滚
|
|
1252
|
+
// currentIdx 会落库「步骤 11 pending + 12-16 completed」半套状态)。内存字段,_write 只写
|
|
1253
|
+
// 固定列不落库。
|
|
1254
|
+
step._batchAligned = true
|
|
1215
1255
|
aligned++
|
|
1216
1256
|
}
|
|
1217
1257
|
}
|
|
@@ -1251,10 +1291,11 @@ export async function waitStep(pm, progress, stageName, cwd, outputText, waitRea
|
|
|
1251
1291
|
process.exit(1)
|
|
1252
1292
|
}
|
|
1253
1293
|
|
|
1254
|
-
// 查找下一个 pending
|
|
1255
|
-
|
|
1294
|
+
// 查找下一个 pending / in-progress / blocked 的步骤(blocked 同为当前步——与 completeStep
|
|
1295
|
+
// 的 findIndex 口径对齐,否则 blocked 后 --wait 落到其后第一个 pending 步上错步记账)
|
|
1296
|
+
const currentIdx = stageData.steps.findIndex(s => s.status === 'pending' || s.status === 'in-progress' || s.status === 'blocked')
|
|
1256
1297
|
if (currentIdx === -1) {
|
|
1257
|
-
console.error(`没有可以等待的步骤(阶段 ${stageName} 已无 pending/in-progress 步骤)。当前阶段状态:${stageData?.status ?? '未知'}。用 \`sillyspec run ${stageName} --status\` 查看进度,或 \`sillyspec progress show\` 看全局下一步。`)
|
|
1298
|
+
console.error(`没有可以等待的步骤(阶段 ${stageName} 已无 pending/in-progress/blocked 步骤)。当前阶段状态:${stageData?.status ?? '未知'}。用 \`sillyspec run ${stageName} --status\` 查看进度,或 \`sillyspec progress show\` 看全局下一步。`)
|
|
1258
1299
|
process.exit(1)
|
|
1259
1300
|
}
|
|
1260
1301
|
|
|
@@ -1513,9 +1554,18 @@ export async function continueStep(pm, progress, stageName, cwd, answer, options
|
|
|
1513
1554
|
if (stageName === 'verify') {
|
|
1514
1555
|
try {
|
|
1515
1556
|
const { consumeFrictionHint } = await import('../friction-tally.js')
|
|
1516
|
-
const r = consumeFrictionHint({ cwd, changeName, platformOpts })
|
|
1557
|
+
const r = await consumeFrictionHint({ cwd, changeName, platformOpts })
|
|
1517
1558
|
if (r && r.hint) console.log(`\n${r.hint}`)
|
|
1518
|
-
|
|
1559
|
+
// 台账滚动(2026-09-15-tax-governance FR-02 主滚动点):consume 得到的非零 counts 按
|
|
1560
|
+
// change 滚动合并进 <runtimeRoot>/friction-ledger.json 幸存台账(merge-by-change——
|
|
1561
|
+
// --reopen 重跑二次 consume 累加非双计);空 counts 跳过写(防干净收尾落空文件);
|
|
1562
|
+
// archivedAt 此时不落(prune 侧落定);runtimeRoot 与 consume 同源(specBase 同变量
|
|
1563
|
+
// + resolveRuntimeRoot 单点)。台账写失败 fail-soft 在既有 try/catch 内,绝不阻断收尾。
|
|
1564
|
+
if (changeName && r && r.counts && Object.keys(r.counts).length > 0) {
|
|
1565
|
+
const { mergeFrictionEntry } = await import('../friction-ledger.js')
|
|
1566
|
+
await mergeFrictionEntry(resolveRuntimeRoot(platformOpts, specBase), { change: changeName, counts: r.counts })
|
|
1567
|
+
}
|
|
1568
|
+
} catch { /* 摩擦提示/台账失败不影响收尾 */ }
|
|
1519
1569
|
}
|
|
1520
1570
|
// 阶段完成后明确下一步(agent 常卡:stageData completed 但不知要 run <下一阶段> 推进 currentStage)
|
|
1521
1571
|
const nextStageHint = { brainstorm: 'plan', plan: 'execute', execute: 'verify', verify: 'archive' }[stageName]
|
|
@@ -1569,7 +1619,10 @@ export async function skipStep(pm, progress, stageName, cwd, changeName, platfor
|
|
|
1569
1619
|
}
|
|
1570
1620
|
|
|
1571
1621
|
const steps = stageData.steps
|
|
1572
|
-
|
|
1622
|
+
// blocked 同为当前步——与 completeStep 的 findIndex 口径对齐(坑 deps-gate-blocked-invisible
|
|
1623
|
+
// 在 completeStep 修过但此处漏同步:blocked 后 --skip 落到其后第一个 pending 步上,错步跳过
|
|
1624
|
+
// 且 blocked 步永久卡死只能 --reset)
|
|
1625
|
+
const currentIdx = steps.findIndex(s => s.status === 'pending' || s.status === 'in-progress' || s.status === 'blocked')
|
|
1573
1626
|
|
|
1574
1627
|
if (currentIdx === -1) {
|
|
1575
1628
|
const wsIdx = steps.findIndex(s => s.status === 'waiting')
|
|
@@ -1577,11 +1630,15 @@ export async function skipStep(pm, progress, stageName, cwd, changeName, platfor
|
|
|
1577
1630
|
console.error(`⏸️ Step ${wsIdx + 1} 正在等待用户输入,不能跳过。`)
|
|
1578
1631
|
console.error(` 请先使用 --continue --answer "..." 继续,或用 --reset 重置。`)
|
|
1579
1632
|
} else {
|
|
1580
|
-
console.error(`没有待跳过的步骤(阶段 ${stageName} 已无 pending/in-progress 步骤)。当前阶段状态:${stageData?.status ?? '未知'}。用 \`sillyspec run ${stageName} --status\` 查看进度,或 \`sillyspec progress show\` 看全局下一步。`)
|
|
1633
|
+
console.error(`没有待跳过的步骤(阶段 ${stageName} 已无 pending/in-progress/blocked 步骤)。当前阶段状态:${stageData?.status ?? '未知'}。用 \`sillyspec run ${stageName} --status\` 查看进度,或 \`sillyspec progress show\` 看全局下一步。`)
|
|
1581
1634
|
}
|
|
1582
1635
|
process.exit(1)
|
|
1583
1636
|
}
|
|
1584
1637
|
|
|
1638
|
+
if (steps[currentIdx].status === 'blocked') {
|
|
1639
|
+
console.log(`⚠️ Step "${steps[currentIdx].name}" 此前被门控阻断(blocked),本次 --skip 将跳过该步——若门控条件已修复,更宜直接 --done 推进。`)
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1585
1642
|
const defSteps = await getStageStepsAutoAware(stageName, cwd, progress, platformOpts?.specRoot || null)
|
|
1586
1643
|
const stepDef = defSteps ? defSteps[currentIdx] : null
|
|
1587
1644
|
if (stepDef && !stepDef.optional) {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* 纯函数无副作用:不写盘、不 console(console 留给调用点 task-02/03)。
|
|
13
13
|
*/
|
|
14
14
|
import { safeGit, parsePorcelainPath, isQuickMetadata } from './shared.js'
|
|
15
|
+
import { basename } from 'node:path'
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* 内联解析 .sillyspec/changes/<dir>/ 路径取 <dir>。
|
|
@@ -151,6 +152,103 @@ export function formatConcurrentWarning(detected) {
|
|
|
151
152
|
for (const c of otherActiveChanges) lines.push(` - ${c}`)
|
|
152
153
|
}
|
|
153
154
|
|
|
154
|
-
lines.push('提交请用显式 pathspec 隔离本变更文件,勿 git add .
|
|
155
|
+
lines.push('提交请用显式 pathspec 隔离本变更文件,勿 git add . 扫入他者工作(execute task 内建议 sillyspec wt-commit 串行提交)。')
|
|
155
156
|
return lines.join('\n')
|
|
156
157
|
}
|
|
158
|
+
|
|
159
|
+
// ── 坑 mixed-baseline-drift-hint(ql-20260915-004)committed-drift 混合基线检测 ──
|
|
160
|
+
// 背景(用户 2026-09-15 实证):并行会话合并断言不同步——worktree 内基于旧快照写的测试断言
|
|
161
|
+
// (旧 id),主仓已合入 id 统一修复 → verify 测试门红——测试源=worktree 快照、被测源=主仓
|
|
162
|
+
// HEAD 的混合基线漂移此前无任何提示。本函数算「主仓自本变更基点后的已提交推进 ∩ 本变更
|
|
163
|
+
// 相关文件」,交两挂点(worktree-apply 成功尾声 / gates.js verify 测试门前)advisory 提示。
|
|
164
|
+
|
|
165
|
+
/** 测试形态文件名:foo.test.mjs / foo.spec.js(与探针 7 isProbe7TestPath 的 .test. 口径同族) */
|
|
166
|
+
const DRIFT_TEST_FORM_RE = /\.(?:test|spec)\.[A-Za-z0-9]+$/
|
|
167
|
+
|
|
168
|
+
/** git 路径反斜杠归一(detectCommittedDrift 内部用,与 normalizeGitPath 同义独立小函数) */
|
|
169
|
+
function toPosix(p) {
|
|
170
|
+
return String(p || '').replace(/\\/g, '/')
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 文件名 stem 键:剥扩展名与 .test./.spec. 中缀(foo.js → foo;foo.test.mjs → foo;
|
|
175
|
+
* foo_test.js → foo_test——下划线形不在变体口径内,仅精确匹配兜底)。小写归一。
|
|
176
|
+
*/
|
|
177
|
+
function driftStemKey(p) {
|
|
178
|
+
const name = basename(toPosix(p))
|
|
179
|
+
const m = name.match(/^(.+?)\.(?:test|spec)\.[A-Za-z0-9]+$/)
|
|
180
|
+
if (m) return m[1].toLowerCase()
|
|
181
|
+
const dot = name.lastIndexOf('.')
|
|
182
|
+
return (dot > 0 ? name.slice(0, dot) : name).toLowerCase()
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 检测混合基线漂移(非阻断 advisory 用):主仓 `git diff --name-only <baseHash>..<head>`
|
|
187
|
+
* (基点后已提交推进面)∩ 本变更 touchedFiles(或其同 basename 的 .test./.spec. 变体)非空
|
|
188
|
+
* → drift。变体口径:stem 相同且至少一侧为测试形态(src/foo.js ↔ test/foo.test.mjs)——
|
|
189
|
+
* 换位覆盖两方向(他者推进我的源文件的测试变体 / 他者推进我测试文件对应的源文件)。
|
|
190
|
+
*
|
|
191
|
+
* fail-open:baseHash 缺失 / git 异常 → 返回 null(调用方视为未检测,零输出零阻断);
|
|
192
|
+
* diff 空(基点后无推进)→ { drift:false, files:[] }。
|
|
193
|
+
*
|
|
194
|
+
* @param {{ projectRoot: string, baseHash: string, touchedFiles: string[],
|
|
195
|
+
* excludeFiles?: string[], head?: string }} opts
|
|
196
|
+
* - baseHash:worktree 基点(meta.baselineCommit || baseHash)
|
|
197
|
+
* - touchedFiles:本变更交付文件面(apply-manifest files / changedFiles)
|
|
198
|
+
* - excludeFiles:从推进面剔除的文件(verify 挂点传本变更自身面——apply 已提交时
|
|
199
|
+
* baseHash..HEAD 含自身交付,不剔会把「自己合自己」误报成他者推进;apply 挂点用
|
|
200
|
+
* 精确 preMergeHead,无需剔除)
|
|
201
|
+
* - head:推进面终点 ref(缺省 HEAD;merge 路径传 merge 前 HEAD)
|
|
202
|
+
* @returns {{ drift: boolean, files: string[], hint?: string }|null}
|
|
203
|
+
*/
|
|
204
|
+
export function detectCommittedDrift({ projectRoot, baseHash, touchedFiles, excludeFiles = [], head = 'HEAD' }) {
|
|
205
|
+
if (!projectRoot || !baseHash) return null
|
|
206
|
+
const touched = (Array.isArray(touchedFiles) ? touchedFiles : []).map(toPosix).filter(Boolean)
|
|
207
|
+
if (touched.length === 0) return null
|
|
208
|
+
// FR fail-open(与 detectConcurrentChanges 同族):git 读不到不崩不误报
|
|
209
|
+
const diff = safeGit(projectRoot, ['diff', '--name-only', `${baseHash}..${head}`], { timeout: 30000 })
|
|
210
|
+
if (diff.error) return null
|
|
211
|
+
if (!diff.value) return { drift: false, files: [] }
|
|
212
|
+
|
|
213
|
+
const excl = new Set((Array.isArray(excludeFiles) ? excludeFiles : []).map(toPosix).filter(Boolean))
|
|
214
|
+
const touchedSet = new Set(touched)
|
|
215
|
+
const touchedStems = new Set(touched.map(driftStemKey).filter(Boolean))
|
|
216
|
+
const touchedHasTestForm = touched.some(f => DRIFT_TEST_FORM_RE.test(basename(f)))
|
|
217
|
+
|
|
218
|
+
const files = []
|
|
219
|
+
for (const raw of diff.value.split('\n').map(s => s.trim()).filter(Boolean)) {
|
|
220
|
+
const p = toPosix(raw)
|
|
221
|
+
if (excl.has(p)) continue
|
|
222
|
+
const exact = touchedSet.has(p)
|
|
223
|
+
// 变体:非精确命中时,stem 相同且至少一侧为测试形态(防 docs/foo.md ↔ test/foo.test.mjs
|
|
224
|
+
// 之外的松散同名误报——两侧都非测试形态的不同目录同名不算相关)
|
|
225
|
+
const variant = !exact
|
|
226
|
+
&& touchedStems.has(driftStemKey(p))
|
|
227
|
+
&& (DRIFT_TEST_FORM_RE.test(basename(p)) || touchedHasTestForm)
|
|
228
|
+
if (exact || variant) files.push(p)
|
|
229
|
+
}
|
|
230
|
+
if (files.length === 0) return { drift: false, files: [] }
|
|
231
|
+
return {
|
|
232
|
+
drift: true,
|
|
233
|
+
files,
|
|
234
|
+
hint: `主仓自基点 ${String(baseHash).slice(0, 8)} 后已合入触及 ${files.length} 个本变更相关文件的推进(${files.slice(0, 5).join('、')}${files.length > 5 ? ' 等' : ''})——worktree 快照内验证过的断言可能漂移,建议合并态复跑相关测试`,
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* committed-drift 检测结果 → 多行 ⚠️ 提示串(两挂点共用文案,与 formatConcurrentWarning 同族)。
|
|
240
|
+
* @param {{ drift: boolean, files?: string[], hint?: string }|null} drift detectCommittedDrift 产物
|
|
241
|
+
* @param {{ tailNote?: string }} [opts] tailNote 追加在首行末的挂点语境注(如 verify 挂点的
|
|
242
|
+
* 「实测若红先做合并态归因」);缺省无
|
|
243
|
+
* @returns {string|null} 无漂移/未检测返回 null
|
|
244
|
+
*/
|
|
245
|
+
export function formatCommittedDriftWarning(drift, opts = {}) {
|
|
246
|
+
if (!drift || !drift.drift) return null
|
|
247
|
+
const files = Array.isArray(drift.files) ? drift.files : []
|
|
248
|
+
const note = opts.tailNote ? `(${opts.tailNote})` : ''
|
|
249
|
+
return [
|
|
250
|
+
'⚠️ 混合基线提示:主仓自本变更基点后已合入触及以下本变更相关文件的推进'
|
|
251
|
+
+ `(${files.length} 个)——worktree 快照内验证过的断言可能漂移,建议合并态复跑相关测试${note}:`,
|
|
252
|
+
...files.map(f => ` - ${f}`),
|
|
253
|
+
].join('\n')
|
|
254
|
+
}
|
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 定向跑」:
|