sillyspec 3.33.0 → 3.33.1
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-flow/SKILL.md +1 -1
- package/.github/workflows/ci.yml +11 -3
- package/AGENTS.md +1 -1
- package/assets/command-cards/flow.md +1 -1
- package/package.json +1 -1
- package/src/archive-delta.js +4 -3
- package/src/change-delete.js +6 -5
- package/src/contract-matrix.js +2 -1
- package/src/cross-placement.js +5 -1
- package/src/db.js +8 -2
- package/src/deleted-ledger.js +2 -1
- package/src/doctor-diagnostics.js +8 -3
- package/src/flow-draft.js +2 -1
- package/src/flow.js +111 -62
- package/src/friction-ledger.js +2 -1
- package/src/friction-tally.js +8 -4
- package/src/handoff.js +2 -1
- package/src/hooks/worktree-guard.js +5 -5
- package/src/index.js +41 -7
- package/src/knowledge-hits.js +2 -1
- package/src/knowledge-vector.js +2 -1
- package/src/progress/consistency-doctor.js +5 -2
- package/src/progress/stage-machine.js +6 -2
- package/src/progress.js +19 -8
- package/src/quicklog.js +194 -9
- package/src/review-dispatch.js +4 -3
- package/src/review-tier.js +2 -1
- package/src/route-hindsight.js +4 -3
- package/src/run/bg-sync.js +10 -3
- package/src/run/command.js +13 -11
- package/src/run/complete-handlers.js +40 -37
- package/src/run/complete.js +12 -8
- package/src/run/gate-snapshot-ledger.js +2 -1
- package/src/run/gate-snapshot.js +57 -11
- package/src/run/gates.js +31 -7
- package/src/run/prompt.js +6 -5
- package/src/run/scan-profile.js +2 -1
- package/src/run/shared.js +109 -5
- package/src/run/stage.js +5 -4
- package/src/run/test-ledger.js +2 -1
- package/src/run/verify-quality-scan.js +11 -5
- package/src/runtime-paths.js +189 -0
- package/src/scan-refresh.js +4 -3
- package/src/scope-audit.js +7 -6
- package/src/sentinel-assertions.js +37 -0
- package/src/spec-sync.js +38 -22
- package/src/stage-contract.js +24 -2
- package/src/stage-review.js +3 -2
- package/src/stages/brainstorm.js +1 -1
- package/src/sync-noise.js +3 -2
- package/src/sync.js +71 -15
- package/src/task-done.js +2 -1
- package/src/task-review.js +7 -5
- package/src/task-tick.js +23 -1
- package/src/validate-artifacts.js +2 -1
- package/src/verify-draft.js +2 -1
- package/src/verify-postcheck.js +11 -10
- package/src/verify-probes.js +157 -52
- package/src/watcher.js +15 -9
- package/src/worktree-apply.js +2 -1
- package/src/write-audit.js +2 -1
- package/templates/agents-instruction.md +1 -1
package/src/review-dispatch.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync } from 'node:fs'
|
|
21
|
+
import { runtimeStatePath } from './runtime-paths.js'
|
|
21
22
|
import { join, dirname } from 'node:path'
|
|
22
23
|
import jsYaml from 'js-yaml'
|
|
23
24
|
import { writeAtomicSync } from './fs-atomic.js'
|
|
@@ -41,7 +42,7 @@ const DISPATCH_STATES = ['dispatching', 'in-flight', 'completed', 'failed', 'aba
|
|
|
41
42
|
|
|
42
43
|
/** 在途记录路径:{SPEC_ROOT}/.runtime/review-dispatch-<change>.json */
|
|
43
44
|
function dispatchRecordPath(specBase, changeName) {
|
|
44
|
-
return join(specBase, '.runtime', `review-dispatch-${changeName}.json`)
|
|
45
|
+
return runtimeStatePath(join(specBase, '.runtime'), `review-dispatch-${changeName}.json`)
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
/** 读在途记录(无/坏 → null,fail-open 调用方按无在途处理)。 */
|
|
@@ -459,7 +460,7 @@ function appendShadowLedgerSkip(runtimeRoot, skip) {
|
|
|
459
460
|
*/
|
|
460
461
|
function readCeremonyTierForShadow(runtimeRoot, changeName) {
|
|
461
462
|
try {
|
|
462
|
-
const p =
|
|
463
|
+
const p = runtimeStatePath(runtimeRoot, `ceremony-tier-${changeName}.json`)
|
|
463
464
|
if (!existsSync(p)) return null
|
|
464
465
|
const doc = JSON.parse(readFileSync(p, 'utf8'))
|
|
465
466
|
if (!doc || typeof doc !== 'object' || Array.isArray(doc)) return null
|
|
@@ -480,7 +481,7 @@ function generateShadowReviewRunId() {
|
|
|
480
481
|
// stage-review.js:751 的主线在途提示内联读的也是主线文件名,不会被影子记录污染)──
|
|
481
482
|
|
|
482
483
|
function shadowRecordPath(specBase, changeName) {
|
|
483
|
-
return join(specBase, '.runtime', `review-dispatch-shadow-${changeName}.json`)
|
|
484
|
+
return runtimeStatePath(join(specBase, '.runtime'), `review-dispatch-shadow-${changeName}.json`)
|
|
484
485
|
}
|
|
485
486
|
|
|
486
487
|
function readShadowRecord(specBase, changeName) {
|
package/src/review-tier.js
CHANGED
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
*/
|
|
36
36
|
|
|
37
37
|
import { parseFileChangeList } from './change-list.js'
|
|
38
|
+
import { runtimeStatePath } from './runtime-paths.js'
|
|
38
39
|
import { computeCeremonyTier, CEREMONY_TIERS } from './ceremony-tier.js'
|
|
39
40
|
import { readCeremonyPricingConfig } from './ceremony-config.js'
|
|
40
41
|
import { resolveChangeRisk, extractExplicitRiskLevel } from './change-risk-profile.js'
|
|
@@ -151,7 +152,7 @@ export function classifyReviewTier({ planLevel, designPath, riskDetection, frict
|
|
|
151
152
|
if (designPath) {
|
|
152
153
|
try {
|
|
153
154
|
const changeDir = dirname(designPath)
|
|
154
|
-
const tierPath = join(dirname(dirname(changeDir)), '.runtime', `ceremony-tier-${basename(changeDir)}.json`)
|
|
155
|
+
const tierPath = runtimeStatePath(join(dirname(dirname(changeDir)), '.runtime'), `ceremony-tier-${basename(changeDir)}.json`)
|
|
155
156
|
if (existsSync(tierPath)) {
|
|
156
157
|
const tf = JSON.parse(readFileSync(tierPath, 'utf8'))
|
|
157
158
|
if (tf && CEREMONY_TIERS.includes(tf.tier)) tierFileTier = tf.tier
|
package/src/route-hindsight.js
CHANGED
|
@@ -37,6 +37,7 @@ import { createHash } from 'node:crypto'
|
|
|
37
37
|
import { join, basename, dirname } from 'node:path'
|
|
38
38
|
import { writeAtomicSync } from './fs-atomic.js'
|
|
39
39
|
import { computeEditRatio } from './flow-draft.js'
|
|
40
|
+
import { runtimeStatePath } from './runtime-paths.js'
|
|
40
41
|
|
|
41
42
|
/** 超阈判定常量(可调;口径:比例两路为严格大于——恰等于阈值不算超,保守防误标 R-01;
|
|
42
43
|
* 计数两路为大于等于——2 维/2 次即系统性信号)。 */
|
|
@@ -107,7 +108,7 @@ function num(v) {
|
|
|
107
108
|
export function snapshotBaseline({ specBase, change, changeDir }) {
|
|
108
109
|
try {
|
|
109
110
|
const runtimeRoot = join(specBase, '.runtime')
|
|
110
|
-
const p =
|
|
111
|
+
const p = runtimeStatePath(runtimeRoot, `${BASELINE_PREFIX}${change}.json`)
|
|
111
112
|
if (existsSync(p)) return { captured: false, reason: 'already-exists(首写者胜)' }
|
|
112
113
|
const payload = {
|
|
113
114
|
schemaVersion: 1,
|
|
@@ -130,7 +131,7 @@ export function snapshotBaseline({ specBase, change, changeDir }) {
|
|
|
130
131
|
*/
|
|
131
132
|
export function baselineSha256({ specBase, change }) {
|
|
132
133
|
try {
|
|
133
|
-
const p = join(specBase, '.runtime', `${BASELINE_PREFIX}${change}.json`)
|
|
134
|
+
const p = runtimeStatePath(join(specBase, '.runtime'), `${BASELINE_PREFIX}${change}.json`)
|
|
134
135
|
if (!existsSync(p)) return null
|
|
135
136
|
return createHash('sha256').update(readTextSafe(p)).digest('hex')
|
|
136
137
|
} catch {
|
|
@@ -161,7 +162,7 @@ export function computeHindsightMetrics({ changeDir, reviewJson, flowState }) {
|
|
|
161
162
|
// ① 首版快照 → 终稿行级改写比
|
|
162
163
|
let designRewriteRatio = 0
|
|
163
164
|
let tasksRewriteRatio = 0
|
|
164
|
-
const baselineRaw = readTextSafe(join(specBase, '.runtime', `${BASELINE_PREFIX}${change}.json`))
|
|
165
|
+
const baselineRaw = readTextSafe(runtimeStatePath(join(specBase, '.runtime'), `${BASELINE_PREFIX}${change}.json`))
|
|
165
166
|
let baseline = null
|
|
166
167
|
if (baselineRaw != null) { try { const j = JSON.parse(baselineRaw); baseline = j && typeof j === 'object' ? j : null } catch { baseline = null } }
|
|
167
168
|
if (baseline) {
|
package/src/run/bg-sync.js
CHANGED
|
@@ -36,12 +36,18 @@
|
|
|
36
36
|
* 刻意不赌);父进程内 shared.js 已加载,动态 import 走缓存无额外成本。
|
|
37
37
|
*/
|
|
38
38
|
import { spawn } from 'child_process';
|
|
39
|
+
import { runtimeTempPath } from '../runtime-paths.js'
|
|
39
40
|
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'fs';
|
|
40
41
|
import { join } from 'path';
|
|
41
42
|
|
|
42
43
|
export const BG_LOCK_FILENAME = 'spec-sync-bg.lock';
|
|
43
44
|
export const BG_LOG_FILENAME = 'spec-sync-bg.log';
|
|
44
45
|
|
|
46
|
+
/** 后台同步日志路径(2026-10-18 布局分层:可随时删除的产物归 temp/)。 */
|
|
47
|
+
export function bgSyncLogPath(runtimeRoot) {
|
|
48
|
+
return runtimeTempPath(runtimeRoot, BG_LOG_FILENAME);
|
|
49
|
+
}
|
|
50
|
+
|
|
45
51
|
// 后台轮参数:单轮预算下限(与 env SILLYSPEC_SYNC_TIMEOUT_MS 取 max)、轮数与总预算帽
|
|
46
52
|
// (防慢平台下无限续轮);锁时效(pid 活性之外的第二道判死——pid 复用假活兜底,
|
|
47
53
|
// 子进程自身总预算 ~2.5min + 收尾余量远小于该值,超时必是异常残留)。
|
|
@@ -108,7 +114,7 @@ export function isBgSyncLockLive(lock, now = Date.now()) {
|
|
|
108
114
|
/** 子进程起步截尾日志(>1MB 保后 512KB,从整行边界起)。失败只影响日志体积。 */
|
|
109
115
|
function truncateLogIfNeeded(runtimeRoot) {
|
|
110
116
|
try {
|
|
111
|
-
const p =
|
|
117
|
+
const p = bgSyncLogPath(runtimeRoot);
|
|
112
118
|
if (statSync(p).size <= LOG_TRUNCATE_THRESHOLD) return;
|
|
113
119
|
const buf = readFileSync(p);
|
|
114
120
|
const keep = buf.subarray(Math.max(0, buf.length - LOG_TRUNCATE_KEEP));
|
|
@@ -143,15 +149,16 @@ export async function spawnBackgroundSync(cwd, changeName, platformOpts = {}, op
|
|
|
143
149
|
// 单飞合并:活后台轮在跑,把「还有新状态要同步」并入其下一轮。置位失败(并发写锁)
|
|
144
150
|
// 只损失合并信号——后台轮收尾锁自清,下一条命令重新 spawn,收敛不丢。
|
|
145
151
|
try { writeBgSyncLock(runtimeRoot, { ...lock, rerunQueued: true }); } catch { /* 见上 */ }
|
|
146
|
-
return { status: 'coalesced', logPath:
|
|
152
|
+
return { status: 'coalesced', logPath: bgSyncLogPath(runtimeRoot) };
|
|
147
153
|
}
|
|
148
154
|
|
|
149
155
|
// 日志 fd:子进程 stdout/stderr 直接 append 进日志文件(跨平台 fd 继承);开不了退 ignore
|
|
150
|
-
const logPath =
|
|
156
|
+
const logPath = bgSyncLogPath(runtimeRoot);
|
|
151
157
|
let stdio = ['ignore', 'ignore', 'ignore'];
|
|
152
158
|
let logFd = null;
|
|
153
159
|
try {
|
|
154
160
|
mkdirSync(runtimeRoot, { recursive: true });
|
|
161
|
+
mkdirSync(join(runtimeRoot, 'temp'), { recursive: true }); // temp/ 可被整体删除,写前重建
|
|
155
162
|
logFd = openSync(logPath, 'a');
|
|
156
163
|
stdio = ['ignore', logFd, logFd];
|
|
157
164
|
} catch {
|
package/src/run/command.js
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* './run/stage.js' → './shared.js' 等同级
|
|
21
21
|
*/
|
|
22
22
|
import { basename, join, resolve, dirname } from 'node:path'
|
|
23
|
+
import { runtimeStatePath } from '../runtime-paths.js'
|
|
23
24
|
import { existsSync, readdirSync, mkdirSync, writeFileSync, readFileSync, rmSync, unlinkSync, statSync } from 'node:fs'
|
|
24
25
|
import { randomBytes, randomUUID } from 'node:crypto'
|
|
25
26
|
import { fileURLToPath } from 'node:url'
|
|
@@ -29,7 +30,7 @@ import { resolveQuickLinkedChanges } from './quick-audit.js'
|
|
|
29
30
|
import { outputStep, collectStageWaitHistory } from './prompt.js'
|
|
30
31
|
import { completeStep, completeStepBurst, skipStep, waitStep, continueStep, synthesizeStepOutput } from './complete.js'
|
|
31
32
|
import { runStage } from './stage.js'
|
|
32
|
-
import { sanitizeDesc,
|
|
33
|
+
import { sanitizeDesc, deriveShortChangeTitle } from '../quicklog.js'
|
|
33
34
|
import { ProgressManager, resolveSessionIdentity } from '../progress.js'
|
|
34
35
|
import { validateChangeExists, checkTransition } from '../stage-contract.js'
|
|
35
36
|
import { READONLY_AUXILIARY_STAGES } from '../constants.js'
|
|
@@ -232,7 +233,7 @@ export async function ensureStageSteps(progress, stageName, cwd, specDir = null)
|
|
|
232
233
|
function emitReadonlyEmptyGuide() {
|
|
233
234
|
console.log('ℹ️ 未找到进度数据(只读查询不建变更)')
|
|
234
235
|
console.log(' → 可开始轻量变更(--input 引号内换行合法,下例可照抄):')
|
|
235
|
-
console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "
|
|
236
|
+
console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "<一句 10-30 字中文概括(首行即变更标题,硬区间 10-30 必须、建议 ~20 字);随后动机与背景')
|
|
236
237
|
console.log('')
|
|
237
238
|
console.log(' 成功标准:')
|
|
238
239
|
console.log(' - <可验证标准>"')
|
|
@@ -858,7 +859,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
858
859
|
quickFallbackUsed = true
|
|
859
860
|
} else {
|
|
860
861
|
console.log(' 本仓无近期 quick 会话记录(current marker 与 quick-sessions 均空)——新工作请走轻量变更(--input 引号内换行合法,可照抄):')
|
|
861
|
-
console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "
|
|
862
|
+
console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "<一句 10-30 字中文概括(首行即变更标题,硬区间 10-30 必须、建议 ~20 字);随后动机与背景')
|
|
862
863
|
console.log('')
|
|
863
864
|
console.log(' 成功标准:')
|
|
864
865
|
console.log(' - <可验证标准>"')
|
|
@@ -1301,11 +1302,12 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
1301
1302
|
}
|
|
1302
1303
|
}
|
|
1303
1304
|
} catch { /* 守卫异常不拦正常注册(fail-open)*/ }
|
|
1304
|
-
// 创建时即写 title(与 name/KEY 同时落 db):--input
|
|
1305
|
-
//
|
|
1305
|
+
// 创建时即写 title(与 name/KEY 同时落 db):--input 首行过提炼 deriveShortChangeTitle
|
|
1306
|
+
// (目标 20/上限 30 自然标点截断;2026-10-16-short-change-title,50 字硬切残句淘汰)
|
|
1307
|
+
// 优先,长输入兜底 sanitizeDesc 压行,无则用 name 兜底。
|
|
1306
1308
|
// proposal 还没写、无权威标题来源,这是临时值;brainstorm/plan 完成 proposal/design 落盘后由
|
|
1307
1309
|
// complete.js 通用完成路径 deriveTitleFromLinkedChange 刷新为真实 # 标题(与 quick 启动 title 同源)。
|
|
1308
|
-
progress = pm.initChange(cwd, autoChange, { title: inputText ? (
|
|
1310
|
+
progress = pm.initChange(cwd, autoChange, { title: inputText ? (deriveShortChangeTitle(inputText) || sanitizeDesc(inputText)) : autoChange })
|
|
1309
1311
|
} else if (isAuxiliary) {
|
|
1310
1312
|
let autoName = changeName || resolveChangeNameAuto(cwd, specRoot) || 'default'
|
|
1311
1313
|
// archive 特例:归档后变更从活跃列表排除(listChanges WHERE status='active'),
|
|
@@ -1332,7 +1334,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
1332
1334
|
changeName = autoName
|
|
1333
1335
|
if (!progress) {
|
|
1334
1336
|
// auxiliary 创建也写 title(--input 优先 / name 兜底),同上完整流程语义。
|
|
1335
|
-
progress = pm.initChange(cwd, autoName, { title: inputText ? (
|
|
1337
|
+
progress = pm.initChange(cwd, autoName, { title: inputText ? (deriveShortChangeTitle(inputText) || sanitizeDesc(inputText)) : autoName })
|
|
1336
1338
|
// initChange 可能因 project 表为空返回 null
|
|
1337
1339
|
if (!progress) {
|
|
1338
1340
|
progress = { currentStage: stageName, stages: {}, lastActive: new Date().toLocaleString('zh-CN', { hour12: false }), project: '' }
|
|
@@ -1354,7 +1356,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
1354
1356
|
// initChangeIfNoActive 单语句原子抢占,败者走同一引导。
|
|
1355
1357
|
const date = new Date().toISOString().slice(0, 10)
|
|
1356
1358
|
const autoName = `${date}-new-change-${randomBytes(4).toString('hex')}`
|
|
1357
|
-
const claim = pm.initChangeIfNoActive(cwd, autoName, { title: inputText ? (
|
|
1359
|
+
const claim = pm.initChangeIfNoActive(cwd, autoName, { title: inputText ? (deriveShortChangeTitle(inputText) || sanitizeDesc(inputText)) : autoName })
|
|
1358
1360
|
if (!claim.created) {
|
|
1359
1361
|
const activeChanges = claim.activeChanges || []
|
|
1360
1362
|
console.error(`❌ 已存在 ${activeChanges.length} 个活跃变更,run brainstorm 无 --change 不会自动创建新变更。`)
|
|
@@ -1377,7 +1379,7 @@ export async function runCommand(args, cwd, specDir = null, opts = {}) {
|
|
|
1377
1379
|
// 正是并发双建的温床)——1 活跃现与多活跃同走 exit 2 引导。
|
|
1378
1380
|
const date = new Date().toISOString().slice(0, 10)
|
|
1379
1381
|
const autoName = `${date}-new-change-${randomBytes(4).toString('hex')}`
|
|
1380
|
-
const claim = pm.initChangeIfNoActive(cwd, autoName, { title: inputText ? (
|
|
1382
|
+
const claim = pm.initChangeIfNoActive(cwd, autoName, { title: inputText ? (deriveShortChangeTitle(inputText) || sanitizeDesc(inputText)) : autoName })
|
|
1381
1383
|
if (!claim.created) {
|
|
1382
1384
|
const activeCount = claim.activeChanges || []
|
|
1383
1385
|
console.error(`❌ 已存在 ${activeCount.length} 个活跃变更,run auto 无法自动定位目标。`)
|
|
@@ -2076,7 +2078,7 @@ async function runAutoMode(pm, progress, cwd, flags, changeName, platformOpts =
|
|
|
2076
2078
|
if (classification.mode === 'quick') {
|
|
2077
2079
|
console.log(`📊 auto 模式分类:${classification.reason} 判为小变更`)
|
|
2078
2080
|
console.log(` 小变更请走轻量变更(quick 通道已退役,v3.30.0 起拒绝新会话;--input 引号内换行合法,可照抄):`)
|
|
2079
|
-
console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "
|
|
2081
|
+
console.log(' sillyspec flow start --change <YYYY-MM-DD-名> --input "<一句 10-30 字中文概括(首行即变更标题,硬区间 10-30 必须、建议 ~20 字);随后动机与背景')
|
|
2080
2082
|
console.log('')
|
|
2081
2083
|
console.log(' 成功标准:')
|
|
2082
2084
|
console.log(' - <可验证标准>"')
|
|
@@ -2382,7 +2384,7 @@ function printAutoCompletionSummary(pm, cwd, changeName, flowStages, specBase) {
|
|
|
2382
2384
|
// last-delta 模块清单(sidecar 缺失跳过)
|
|
2383
2385
|
try {
|
|
2384
2386
|
const runtimeRoot = join((specBase || join(cwd, '.sillyspec')), '.runtime')
|
|
2385
|
-
const sidecarPath =
|
|
2387
|
+
const sidecarPath = runtimeStatePath(runtimeRoot, 'last-delta.json')
|
|
2386
2388
|
if (existsSync(sidecarPath)) {
|
|
2387
2389
|
const sidecar = JSON.parse(readFileSync(sidecarPath, 'utf8'))
|
|
2388
2390
|
if (Array.isArray(sidecar.affectedModules) && sidecar.affectedModules.length > 0) {
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
* (非被外层吞);process.exit 不可被 try 捕获 → 搬迁行为完全等价。
|
|
31
31
|
*/
|
|
32
32
|
import { basename, dirname, join, resolve, relative, isAbsolute } from 'node:path'
|
|
33
|
+
import { runtimeStatePath, runtimeStateFileNames } from '../runtime-paths.js'
|
|
33
34
|
import { existsSync, readdirSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, rmSync, renameSync } from 'node:fs'
|
|
34
35
|
import { createHash } from 'node:crypto'
|
|
35
36
|
import { renameSyncRetry, writeAtomicSync } from '../fs-atomic.js'
|
|
@@ -161,8 +162,9 @@ function ownerFromReviewedFiles(reviewedFiles) {
|
|
|
161
162
|
* 归档 / change-delete 无 delta 同样可删。
|
|
162
163
|
*
|
|
163
164
|
* 归属 fail-closed(禁 mtime 猜、禁后缀匹配):
|
|
164
|
-
*
|
|
165
|
-
*
|
|
165
|
+
* state/ 全家族散文件 + 根目录同名兜底(runtime-paths.js runtimeStateFileNames 唯一清单:
|
|
166
|
+
* draft-ledger / watcher 三件套 / test 台账 / ceremony 档位 / 冲突标记 / marker 等,
|
|
167
|
+
* 含 apply-pathspec-<change>.txt、prompt-inject-<change>.json 两个历史登记家族)
|
|
166
168
|
* execute-runs/<runId>/ 仅当 `change` 戳全等(无戳旧 run 留下,不按覆盖度启发式删)
|
|
167
169
|
* stage-reviews/<dir>/ 仅当 review.json reviewedFiles 首段 === changeName
|
|
168
170
|
* verify-runs/<ts>/ 仅当目录内 JSON 的 change 字段集合 size=1 且等于本变更
|
|
@@ -193,33 +195,26 @@ export function pruneArchivedChangeRuntime(runtimeRoot, changeName) {
|
|
|
193
195
|
} catch { /* 单份失败不连坐,下次归档/删变更再收 */ }
|
|
194
196
|
}
|
|
195
197
|
|
|
198
|
+
// 2026-10-18 布局分层:per-change 散文件全部落 state/,回收名单从三家族扩到全家族
|
|
199
|
+
// (runtime-paths.js STATE_FAMILIES 唯一清单源,禁 glob 的精确名形态不变)。
|
|
200
|
+
// 每个精确名试双路径:state/(新布局)+ 根目录(未迁移旧仓/混版本迟到文件兜底——
|
|
201
|
+
// 迁移幂等收敛后根路径命中恒为零)。friction-tally 删前残余滚台账语义保留(见函数头)。
|
|
196
202
|
try {
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
// 删前残余先滚台账(tax-governance FR-02 兜底滚动点,见函数头注释)。
|
|
213
|
-
try {
|
|
214
|
-
const fp = join(runtimeRoot, `friction-tally-${changeName}.json`)
|
|
215
|
-
if (existsSync(fp)) {
|
|
216
|
-
const merged = mergeFrictionEntrySync(runtimeRoot, {
|
|
217
|
-
change: changeName,
|
|
218
|
-
counts: residualFrictionCounts(fp),
|
|
219
|
-
archivedAt: new Date().toISOString(),
|
|
220
|
-
})
|
|
221
|
-
ledgerAppend = !!(merged && merged.ok && !merged.skipped)
|
|
222
|
-
gone(fp)
|
|
203
|
+
for (const name of runtimeStateFileNames(changeName)) {
|
|
204
|
+
for (const p of [runtimeStatePath(runtimeRoot, name), join(runtimeRoot, name)]) {
|
|
205
|
+
try {
|
|
206
|
+
if (!existsSync(p)) continue
|
|
207
|
+
if (name === `friction-tally-${changeName}.json`) {
|
|
208
|
+
const merged = mergeFrictionEntrySync(runtimeRoot, {
|
|
209
|
+
change: changeName,
|
|
210
|
+
counts: residualFrictionCounts(p),
|
|
211
|
+
archivedAt: new Date().toISOString(),
|
|
212
|
+
})
|
|
213
|
+
ledgerAppend = !!(merged && merged.ok && !merged.skipped)
|
|
214
|
+
}
|
|
215
|
+
gone(p)
|
|
216
|
+
} catch { /* 单路径失败不连坐 */ }
|
|
217
|
+
}
|
|
223
218
|
}
|
|
224
219
|
} catch {}
|
|
225
220
|
|
|
@@ -320,15 +315,22 @@ export async function archiveWorktreeCleanup(cwd, archiveChangeName, specBase, p
|
|
|
320
315
|
// 手工短名变更(如 login)时误删自动日期前缀变更(2026-08-01-login 天然以 '-login' 结尾)
|
|
321
316
|
// 的 marker。stage 集合与写入侧 stageReviewMarkerPath 的调用方一致。
|
|
322
317
|
const REVIEW_STAGES = ['brainstorm', 'plan', 'execute', 'verify', 'archive']
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
)
|
|
327
|
-
|
|
328
|
-
try {
|
|
318
|
+
// 2026-10-18 布局分层后 marker 落 state/;根目录扫描保留为旧布局兜底(迁移幂等收敛)
|
|
319
|
+
const markerDirs = [join(runtimeRoot, 'state'), runtimeRoot]
|
|
320
|
+
let markerCleaned = 0
|
|
321
|
+
for (const markerDir of markerDirs) {
|
|
322
|
+
let names = []
|
|
323
|
+
try { names = readdirSync(markerDir) } catch { continue }
|
|
324
|
+
const markers = names.filter((f) =>
|
|
325
|
+
f === `current-execute-run-id-${archiveChangeName}` ||
|
|
326
|
+
REVIEW_STAGES.some(st => f === `current-stage-review-run-id-${st}-${archiveChangeName}`),
|
|
327
|
+
)
|
|
328
|
+
for (const m of markers) {
|
|
329
|
+
try { unlinkSync(join(markerDir, m)); markerCleaned++ } catch {}
|
|
330
|
+
}
|
|
329
331
|
}
|
|
330
|
-
if (
|
|
331
|
-
console.log(`🧹 归档清理 ${
|
|
332
|
+
if (markerCleaned > 0) {
|
|
333
|
+
console.log(`🧹 归档清理 ${markerCleaned} 个 runId marker(execute/stage-review)`)
|
|
332
334
|
}
|
|
333
335
|
// 取证回收必须在下方 worktree 块的 `if (!meta) return` 之前:无 meta 早退是常态
|
|
334
336
|
// (apply 已 cleanup),漏接就会让 execute-runs/verify-runs 继续按变更数累积。
|
|
@@ -2803,7 +2805,8 @@ export async function handleExecuteWorktreeCleanup({ stageName, changeName, cwd
|
|
|
2803
2805
|
try {
|
|
2804
2806
|
const runtimeDir = join(cwd, '.sillyspec', '.runtime');
|
|
2805
2807
|
mkdirSync(runtimeDir, { recursive: true });
|
|
2806
|
-
|
|
2808
|
+
try { mkdirSync(join(runtimeDir, 'state'), { recursive: true }); } catch { /* 回执 fail-open */ }
|
|
2809
|
+
writeFileSync(runtimeStatePath(runtimeDir, `execute-cleanup-${changeName}.json`), JSON.stringify({
|
|
2807
2810
|
change: changeName, cleanedAt: new Date().toISOString(),
|
|
2808
2811
|
reason: 'execute-completed-no-unapplied-changes',
|
|
2809
2812
|
}, null, 2) + '\n', 'utf8');
|
package/src/run/complete.js
CHANGED
|
@@ -17,12 +17,13 @@
|
|
|
17
17
|
* - node: join(path) + existsSync/readdirSync/readFileSync/mkdirSync/writeFileSync/appendFileSync/statSync(fs)
|
|
18
18
|
*/
|
|
19
19
|
import { join, isAbsolute, resolve } from 'node:path'
|
|
20
|
+
import { runtimeStatePath } from '../runtime-paths.js'
|
|
20
21
|
import { existsSync, readFileSync, mkdirSync, writeFileSync, appendFileSync } from 'node:fs'
|
|
21
22
|
import { writeAtomicSync } from '../fs-atomic.js'
|
|
22
23
|
import { gitQuiet } from '../git-helper.js'
|
|
23
24
|
import { writeCloseTraceArtifacts, projectTraceFaceRows } from '../flow-parity.js'
|
|
24
25
|
import { withFileLock } from '../quicklog.js'
|
|
25
|
-
import { triggerSync, WAIT_MARKER_RE, getStageSteps, formatWaitOptions, resolveRuntimeRoot, getOrCreateMultiRepoContext, resolveChangeDir, writePlatformDocsPointer, isRetiredAutoBrainstormTable, printRetiredAutoBrainstormRefusal } from './shared.js'
|
|
26
|
+
import { triggerSync, triggerSyncTerminalStrict, WAIT_MARKER_RE, getStageSteps, formatWaitOptions, resolveRuntimeRoot, getOrCreateMultiRepoContext, resolveChangeDir, writePlatformDocsPointer, isRetiredAutoBrainstormTable, printRetiredAutoBrainstormRefusal } from './shared.js'
|
|
26
27
|
import { isExplicitReviewWrite, collectWorktreeChangedFiles } from '../task-review.js'
|
|
27
28
|
|
|
28
29
|
// 退役自动道守门(2026-10-14-brainstorm-auto-retire):brainstorm-auto 4 步表已随自动道删除。
|
|
@@ -828,7 +829,10 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
828
829
|
|
|
829
830
|
// gate 全过:persist completed(task-01 移后)。此处到 _write 之间若崩,DB 仍 in-progress(内存已 completed 但未落盘),下次进 CLI 读 DB 即 in-progress,不产生"假 completed"。
|
|
830
831
|
pm._write(cwd, progress, changeName)
|
|
831
|
-
|
|
832
|
+
// 终态强校验(2026-10-17-archive-half-state-location-stuck-active):状态机驱动——仅
|
|
833
|
+
// DB status 已达 archived/deleted(archive 阶段完成即此态)走内联强校验,其余阶段
|
|
834
|
+
// 原样委托 triggerSync(后台化不变,非终态零行为变化)。见 run/shared.js 契约注释。
|
|
835
|
+
await triggerSyncTerminalStrict(cwd, changeName, platformOpts, { completion: true })
|
|
832
836
|
|
|
833
837
|
const total = steps.length
|
|
834
838
|
console.log(`✅ ${stageName} 阶段已完成(${total}/${total} 步)`)
|
|
@@ -843,7 +847,7 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
843
847
|
let stageLedger = null
|
|
844
848
|
let crossStage = false
|
|
845
849
|
try {
|
|
846
|
-
const ledgerPath = join(specBase, '.runtime', `stage-session-ledger-${changeName}.json`)
|
|
850
|
+
const ledgerPath = runtimeStatePath(join(specBase, '.runtime'), `stage-session-ledger-${changeName}.json`)
|
|
847
851
|
try { stageLedger = JSON.parse(readFileSync(ledgerPath, 'utf8')) } catch { /* 首阶段 */ }
|
|
848
852
|
const r = updateStageSessionLedger(stageLedger, process.env.SILLYSPEC_SESSION_ID, stageName)
|
|
849
853
|
stageLedger = r.ledger
|
|
@@ -1018,7 +1022,7 @@ export async function completeStep(pm, progress, stageName, cwd, outputText, inp
|
|
|
1018
1022
|
if (stageName === 'execute' && changeName && defSteps && defSteps[currentIdx] && defSteps[nextPendingIdx] &&
|
|
1019
1023
|
/Wave \d+ 执行/.test(defSteps[currentIdx].name || '') && /Wave \d+ 执行/.test(defSteps[nextPendingIdx].name || '')) {
|
|
1020
1024
|
try {
|
|
1021
|
-
const ledgerPath = join(specBase, '.runtime', `wave-session-ledger-${changeName}.json`)
|
|
1025
|
+
const ledgerPath = runtimeStatePath(join(specBase, '.runtime'), `wave-session-ledger-${changeName}.json`)
|
|
1022
1026
|
let ledger = null
|
|
1023
1027
|
try { ledger = JSON.parse(readFileSync(ledgerPath, 'utf8')) } catch { /* 首 Wave */ }
|
|
1024
1028
|
const { ledger: nextLedger, crossWaveAdvisory } = updateWaveSessionLedger(ledger, process.env.SILLYSPEC_SESSION_ID, defSteps[currentIdx].name)
|
|
@@ -1141,7 +1145,7 @@ async function printVerifyScopeDrift({ cwd, changeName, specBase, platformOpts,
|
|
|
1141
1145
|
snapshot = JSON.parse(readFileSync(join(specBase, 'changes', changeName, 'scope-audit.json'), 'utf8'))
|
|
1142
1146
|
} catch {
|
|
1143
1147
|
try {
|
|
1144
|
-
snapshot = JSON.parse(readFileSync(
|
|
1148
|
+
snapshot = JSON.parse(readFileSync(runtimeStatePath(resolveRuntimeRoot(platformOpts, specBase), `scope-audit-${changeName}.json`), 'utf8'))
|
|
1145
1149
|
} catch { /* 快照缺失/损坏 → 跳过对比只打当前 totals */ }
|
|
1146
1150
|
}
|
|
1147
1151
|
}
|
|
@@ -1559,7 +1563,7 @@ export async function autoTickTasksFromCommitSubjects({ changeName, cwd, specBas
|
|
|
1559
1563
|
try {
|
|
1560
1564
|
const _tasksMdPath = join(specBase, 'changes', changeName, 'tasks.md')
|
|
1561
1565
|
if (!existsSync(_tasksMdPath)) return { ticked: 0, skippedNoAnchor: 0 }
|
|
1562
|
-
const _runIdFile =
|
|
1566
|
+
const _runIdFile = runtimeStatePath(resolveRuntimeRoot(platformOpts, specBase), `current-execute-run-id-${changeName}`)
|
|
1563
1567
|
let _tickLog = ''
|
|
1564
1568
|
try {
|
|
1565
1569
|
if (existsSync(_runIdFile)) {
|
|
@@ -1640,7 +1644,7 @@ export async function autoCheckPlanFromReviews({ stageName, changeName, cwd, pla
|
|
|
1640
1644
|
// plan.md Wave 引用行无 checkbox,无处可勾)。tasks.md 存在性作前置。
|
|
1641
1645
|
const tasksMdPath = join(changeDir, 'tasks.md')
|
|
1642
1646
|
const runtimeRoot = resolveRuntimeRoot(platformOpts, specBaseLc)
|
|
1643
|
-
const runIdFile =
|
|
1647
|
+
const runIdFile = runtimeStatePath(runtimeRoot, `current-execute-run-id-${changeName}`)
|
|
1644
1648
|
if (!existsSync(tasksMdPath) || !existsSync(runIdFile)) {
|
|
1645
1649
|
return { autoChecked: false, checkedCount: 0, skippedCount: 0 }
|
|
1646
1650
|
}
|
|
@@ -1798,7 +1802,7 @@ export async function detectExecuteBatchFinish({ pm, stageName, changeName, cwd,
|
|
|
1798
1802
|
// 平台模式 runtimeRoot / worktree 漂移 specDriftAnchor 不透传时 marker 恒读不到
|
|
1799
1803
|
// → 每个 task 进 blockedTasks → 批量完成静默失效
|
|
1800
1804
|
const runtimeRoot = resolveRuntimeRoot(platformOpts, specBase)
|
|
1801
|
-
const runIdFile =
|
|
1805
|
+
const runIdFile = runtimeStatePath(runtimeRoot, `current-execute-run-id-${changeName}`)
|
|
1802
1806
|
// ── 缺席草稿自动补齐(坑 task-review-retired-archive-mismatch,2026-10-15 归档门口实证)──
|
|
1803
1807
|
// Task Review 层退役(2026-09-26)后 execute 批量完成不再产 per-task review.json,但本函数
|
|
1804
1808
|
// 下方复核循环与 archive step2 完成度判定(真相源=review.json verdict)仍以它在场为前提——
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* 异常面:全部 fail-open 吞掉(账本/回收任一异常零阻断,退现状残留)。
|
|
17
17
|
*/
|
|
18
18
|
import { execFileSync } from 'node:child_process'
|
|
19
|
+
import { runtimeStatePath } from '../runtime-paths.js'
|
|
19
20
|
import { existsSync, mkdirSync, readFileSync } from 'node:fs'
|
|
20
21
|
import { basename, dirname, join, resolve, sep } from 'node:path'
|
|
21
22
|
import { tmpdir } from 'node:os'
|
|
@@ -28,7 +29,7 @@ const STALE_HOURS_DEFAULT = 24
|
|
|
28
29
|
|
|
29
30
|
/** 账本文件路径(runtime 域:.sillyspec/.runtime/active-gate-snapshots.json) */
|
|
30
31
|
export function gateSnapshotLedgerPath(runtimeRoot) {
|
|
31
|
-
return
|
|
32
|
+
return runtimeStatePath(runtimeRoot, LEDGER_FILE)
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
/** 读账本(损坏/缺失/非数组 → 空数组,fail-open) */
|
package/src/run/gate-snapshot.js
CHANGED
|
@@ -494,7 +494,12 @@ export function applyGateSnapshotCopy(cwd, snapshotRoot) {
|
|
|
494
494
|
* @param {{ cwd: string, files: string[] }} opts cwd=主仓根;files=本会话变更文件(仓库根相对 POSIX)
|
|
495
495
|
* @returns {{ snapshotRoot: string, cleanup: () => void, reason?: string }|null} 失败返回 null(调用方回退主仓)
|
|
496
496
|
*/
|
|
497
|
-
export function createGateSnapshot({ cwd, files, sourceRoot = null, skipImportSmoke = false, mergeBase = null, runtimeRoot = null }) {
|
|
497
|
+
export function createGateSnapshot({ cwd, files, sourceRoot = null, skipImportSmoke = false, mergeBase = null, runtimeRoot = null, failRef = null }) {
|
|
498
|
+
// failRef 可选出参(坑 snapshot-fallback-reason-blind,2026-10-16 下游实证):所有失败分支
|
|
499
|
+
// 把结构化原因写进 failRef.reason——调用方(verify --done 实测门 / noAI 质量扫描)消费它
|
|
500
|
+
// 替换硬编码笼统文案,降级原因可直读(pnpm 布局拒建 ≠ 文件集为空 ≠ junction 失败)。
|
|
501
|
+
// 返回契约不变(失败恒 null);不传 failRef 行为与既有逐字节一致。
|
|
502
|
+
const setFail = (reason) => { if (failRef && typeof failRef === 'object') failRef.reason = reason }
|
|
498
503
|
let snapshotRoot = null
|
|
499
504
|
try {
|
|
500
505
|
// 前置:主仓须是 git 仓且有 HEAD(无 git 环境回退主仓现行为)
|
|
@@ -519,6 +524,7 @@ export function createGateSnapshot({ cwd, files, sourceRoot = null, skipImportSm
|
|
|
519
524
|
// 与其让沙箱报无关错误逼 advisory,不如布局命中即作废快照回退主仓(宁可主仓口径)。
|
|
520
525
|
const layoutHit = detectSymlinkStoreLayout(cwd)
|
|
521
526
|
if (layoutHit) {
|
|
527
|
+
setFail(`symlink-store 布局拒建(${layoutHit}):node_modules 符号链接 store 经 junction 跨根失效,实测必假败——设计性回退主仓实测`)
|
|
522
528
|
console.warn(`⚠️ 门禁快照对 ${layoutHit} 布局不可靠(node_modules 符号链接 store 经 junction 跨根失效,实测必假败)——跳过快照回退主仓实测;主仓失败再做污染归属鉴定`)
|
|
523
529
|
return null
|
|
524
530
|
}
|
|
@@ -625,6 +631,7 @@ export function createGateSnapshot({ cwd, files, sourceRoot = null, skipImportSm
|
|
|
625
631
|
// 回退主仓现行为(主仓口径可能被并行脏文件污染,但环境真实;两害取轻 + 回退原因可见)。
|
|
626
632
|
const envMissing = envDirsLinked(cwd, snapshotRoot)
|
|
627
633
|
if (envMissing.length > 0) {
|
|
634
|
+
setFail(`环境目录链接不完整(${envMissing.join('、')} 在主仓存在、快照内缺失)——快照作废回退主仓实测`)
|
|
628
635
|
console.warn(`⚠️ 门禁快照环境不完整(${envMissing.join('、')} 在主仓存在、快照内缺失)——快照作废回退主仓实测(宁可主仓口径不可假沙箱硬挂;回退后失败先做污染归属鉴定)`)
|
|
629
636
|
// 与正常 cleanup/失败 catch 同一清理体:双清确认才销账(该早退曾残留旧式 --quiet
|
|
630
637
|
// 清理与无重试 rmSync——独立审查 P1-1 根因未除尽的漏网面)
|
|
@@ -723,7 +730,10 @@ export function createGateSnapshot({ cwd, files, sourceRoot = null, skipImportSm
|
|
|
723
730
|
return { snapshotRoot, cleanup, overlaid, pinnedCacheLinks }
|
|
724
731
|
} catch (e) {
|
|
725
732
|
// 基建失败(worktree add 拒绝/磁盘满/…):尽力清理后回退主仓;销账同走双清确认
|
|
726
|
-
// (D-004@v2
|
|
733
|
+
// (D-004@v2:清不掉的条目保留待回收——它不是悬空条目,是待回收记录)。
|
|
734
|
+
// 异常细节不再静默(坑 snapshot-fallback-reason-blind):failRef 带原因 + warn 落日志。
|
|
735
|
+
setFail(`快照基建异常: ${e && e.message ? e.message : e}`)
|
|
736
|
+
try { console.warn(`⚠️ 门禁快照基建失败(回退主仓实测): ${e && e.message ? e.message : e}`) } catch { /* 输出失败不影响回退 */ }
|
|
727
737
|
if (snapshotRoot) {
|
|
728
738
|
try {
|
|
729
739
|
const { dirRemoved, worktreeCleaned } = cleanupSnapshot({ snapshotRoot, cwd })
|
|
@@ -870,7 +880,8 @@ export function printSnapshotFailureHint(snapInfo, { offEnv = 'SILLYSPEC_VERIFY_
|
|
|
870
880
|
* @param {{ cwd: string, changeName: string, specBase: string, platformOpts?: object }} opts
|
|
871
881
|
* @returns {Promise<{snapshotRoot: string, cleanup: () => void, overlaid: number, changeFileCount: number}|null>}
|
|
872
882
|
*/
|
|
873
|
-
export async function createVerifyGateSnapshot({ cwd, changeName, specBase, platformOpts = {} }) {
|
|
883
|
+
export async function createVerifyGateSnapshot({ cwd, changeName, specBase, platformOpts = {}, failRef = null }) {
|
|
884
|
+
const setFail = (reason) => { if (failRef && typeof failRef === 'object') failRef.reason = reason }
|
|
874
885
|
try {
|
|
875
886
|
const { resolveVerifyChangedFiles } = await import('../verify-postcheck.js')
|
|
876
887
|
const { resolveRuntimeRoot } = await import('./shared.js')
|
|
@@ -879,10 +890,19 @@ export async function createVerifyGateSnapshot({ cwd, changeName, specBase, plat
|
|
|
879
890
|
let verifyRuntimeRoot = null
|
|
880
891
|
try { verifyRuntimeRoot = resolveRuntimeRoot(platformOpts, specBase) } catch { verifyRuntimeRoot = null }
|
|
881
892
|
const { splitOwnVsForeignDiffFiles } = await import('../foreign-declared.js')
|
|
882
|
-
|
|
893
|
+
// 变更文件集为空三分成因区分(坑 snapshot-fallback-reason-blind):null=git 不可用/解析
|
|
894
|
+
// 失败;[]=HEAD 无 diff;他者声明过滤清空在下方单独计——三者处置同为回退主仓,但排查
|
|
895
|
+
// 出路完全不同,笼统「变更文件集为空」曾把 pnpm 布局拒建也误标成它。
|
|
896
|
+
const rawChangeFiles = resolveVerifyChangedFiles(cwd, changeName, null, {
|
|
883
897
|
specBase, includeWorkingTree: true,
|
|
884
|
-
})
|
|
885
|
-
|
|
898
|
+
})
|
|
899
|
+
let changeFiles = rawChangeFiles || []
|
|
900
|
+
if (changeFiles.length === 0) {
|
|
901
|
+
setFail(rawChangeFiles === null
|
|
902
|
+
? '变更文件集为空(git 不可用/解析失败——非 git 仓或 git 异常,无从定向 overlay)'
|
|
903
|
+
: '变更文件集为空(HEAD 无 diff——改动可能已全部提交,无可隔离面)')
|
|
904
|
+
return null // 无变更文件集 → 无从定向,回退主仓
|
|
905
|
+
}
|
|
886
906
|
// 他者声明归属过滤(坑 verify-reconcile-foreign-wip 同款):in-place 模式 working-tree
|
|
887
907
|
// 并入后主仓全部在途文件进场——他者活跃变更显式声明的文件(quick --files / 他者 design
|
|
888
908
|
// 清单)剔除,只留本变更相关面(无主文件保留,fail-closed 口径与 probe6 一致)。
|
|
@@ -891,7 +911,10 @@ export async function createVerifyGateSnapshot({ cwd, changeName, specBase, plat
|
|
|
891
911
|
if (foreign.length > 0) {
|
|
892
912
|
const foreignSet = new Set(foreign.map(x => x.file))
|
|
893
913
|
changeFiles = changeFiles.filter(f => !foreignSet.has(f))
|
|
894
|
-
if (changeFiles.length === 0)
|
|
914
|
+
if (changeFiles.length === 0) {
|
|
915
|
+
setFail(`变更文件集为空(${foreign.length} 个 diff 文件全部被他者声明过滤——并行会话声明面覆盖了本变更全部在途文件)`)
|
|
916
|
+
return null
|
|
917
|
+
}
|
|
895
918
|
}
|
|
896
919
|
} catch { /* 过滤失败退全量(fail-closed:宁可多 overlay 不漏本变更文件) */ }
|
|
897
920
|
|
|
@@ -920,8 +943,12 @@ export async function createVerifyGateSnapshot({ cwd, changeName, specBase, plat
|
|
|
920
943
|
}
|
|
921
944
|
} catch { /* meta 读取失败 → 主仓 cwd 源(files 已含本变更 working-tree 改动) */ }
|
|
922
945
|
|
|
923
|
-
const snap = createGateSnapshot({ cwd, files: changeFiles, sourceRoot, mergeBase: snapMergeBase, runtimeRoot: verifyRuntimeRoot })
|
|
924
|
-
if (!snap)
|
|
946
|
+
const snap = createGateSnapshot({ cwd, files: changeFiles, sourceRoot, mergeBase: snapMergeBase, runtimeRoot: verifyRuntimeRoot, failRef })
|
|
947
|
+
if (!snap) {
|
|
948
|
+
// createGateSnapshot 已把具体原因写进同一 failRef;无 reason(旧形态兜底)补笼统口径
|
|
949
|
+
if (failRef && typeof failRef === 'object' && !failRef.reason) setFail('快照基建不可用(createGateSnapshot 返回 null,无更细原因)')
|
|
950
|
+
return null
|
|
951
|
+
}
|
|
925
952
|
|
|
926
953
|
// 变更文档随快照:module-impact.md(test_strategy=evidence-auto 消费)/tasks.md 等
|
|
927
954
|
// 在 .sillyspec(gitignore)不进 HEAD,从主仓 specBase 复制 changes/<name>/**
|
|
@@ -940,7 +967,26 @@ export async function createVerifyGateSnapshot({ cwd, changeName, specBase, plat
|
|
|
940
967
|
} catch { /* 文档复制失败不连坐快照(test_strategy 降级 module 集仍可跑) */ }
|
|
941
968
|
|
|
942
969
|
return { ...snap, changeFileCount: changeFiles.length, sourceRoot }
|
|
943
|
-
} catch {
|
|
944
|
-
|
|
970
|
+
} catch (e) {
|
|
971
|
+
// 任一链路异常 → 回退主仓现行为。异常细节不再静默(坑 snapshot-fallback-reason-blind):
|
|
972
|
+
// failRef 带原因 + warn 落日志,排查不再靠 stdout 考古。
|
|
973
|
+
setFail(`快照链路异常: ${e && e.message ? e.message : e}`)
|
|
974
|
+
try { console.warn(`⚠️ 门禁快照链路异常(回退主仓实测): ${e && e.stack ? e.stack.split('\n')[0] : e}`) } catch { /* 输出失败不影响回退 */ }
|
|
975
|
+
return null
|
|
945
976
|
}
|
|
946
977
|
}
|
|
978
|
+
|
|
979
|
+
/**
|
|
980
|
+
* 快照口径观测字段(坑 snapshot-fallback-reason-blind,FR-04 落盘面):--done 实测门把
|
|
981
|
+
* used_snapshot 与降级原因经 runVerifyTestCheck 的 observability 通道带进 test-result.json
|
|
982
|
+
* extra(对齐 2026-10-09 复用观测先例)。快照在场时记录随快照销毁(无可降级事实);
|
|
983
|
+
* 回退主仓时记录恰好落在主仓 .runtime——降级证据存活在主仓,与用途一致。
|
|
984
|
+
* @param {boolean} used 快照是否真实在场
|
|
985
|
+
* @param {{ reason?: string }|null} failRef createVerifyGateSnapshot 的出参(可 null)
|
|
986
|
+
* @returns {{ used_snapshot: boolean, snapshot_fallback_reason?: string }}
|
|
987
|
+
*/
|
|
988
|
+
export function snapshotObservability(used, failRef) {
|
|
989
|
+
if (used) return { used_snapshot: true }
|
|
990
|
+
const reason = failRef && failRef.reason ? String(failRef.reason) : 'createVerifyGateSnapshot 返回 null(变更文件集为空 / 快照基建不可用)'
|
|
991
|
+
return { used_snapshot: false, snapshot_fallback_reason: reason }
|
|
992
|
+
}
|