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/worktree-apply.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* 1. 读取 meta.json 获取 baseHash
|
|
6
6
|
* 2. git diff --name-only baseHash 获取 worktree 中所有变更文件
|
|
7
7
|
* 3. 从 design.md 解析文件变更清单(无清单 = 允许所有)
|
|
8
|
-
* 4. 校验:变更文件 ⊆
|
|
8
|
+
* 4. 校验:变更文件 ⊆ 清单(review 声明与 allow 面相交过滤,D-003:外来声明不并入放行面)
|
|
9
9
|
* 5. 校验:主工作区文件 base hash 一致
|
|
10
10
|
* 6. --check-only 模式只输出检查结果
|
|
11
11
|
* 7. 非 checkOnly:生成 patch → apply --check → apply --3way
|
|
@@ -15,12 +15,15 @@
|
|
|
15
15
|
import { existsSync, unlinkSync, writeFileSync, mkdtempSync, rmSync, readdirSync, readFileSync, mkdirSync } from 'fs';
|
|
16
16
|
import { join, resolve } from 'path';
|
|
17
17
|
import { tmpdir } from 'os';
|
|
18
|
+
import { createHash } from 'crypto';
|
|
18
19
|
import { WorktreeManager } from './worktree.js';
|
|
19
20
|
import { getCrossWorktreeMeta, cleanupCrossWorktrees } from './worktree-cross.js';
|
|
20
21
|
import { parseFileChangeList, parseFileChangeListDetailed, pathMatches } from './change-list.js';
|
|
21
22
|
import { parseAllowedPaths, parseRepo } from './stages/plan-postcheck.js';
|
|
22
|
-
import { git, gitQuiet } from './git-helper.js';
|
|
23
|
+
import { git, gitQuiet, safeGit } from './git-helper.js';
|
|
23
24
|
import { resolveLatestExecuteRunId, resolveLatestExecuteRunIdWithTasks, readReview, normalizeRepoKey } from './task-review.js';
|
|
25
|
+
import { collectActiveQuickGuardFiles } from './quicklog.js';
|
|
26
|
+
import { detectCommittedDrift, formatCommittedDriftWarning } from './run/concurrent-detect.js';
|
|
24
27
|
|
|
25
28
|
const CHANGES_REL = '.sillyspec/changes';
|
|
26
29
|
|
|
@@ -131,9 +134,131 @@ export function mergeDirtyOverlapThreeWay({ projectRoot, worktreePath, baseHash
|
|
|
131
134
|
if (code < 0) { conflicts.push(f); continue } // merge-file 本身不可用 → 原拦截
|
|
132
135
|
}
|
|
133
136
|
if (code === 0) {
|
|
137
|
+
// 覆写前备份主仓在途原文(2026-09-12 审查批 D-②):合并写回发生在 apply 整体成败
|
|
138
|
+
// 判定之前——后续步骤(5a/7)失败返回时主仓已是合并版,而原文从未进任何 git 对象
|
|
139
|
+
// (也未 commit),不留备份即不可恢复。备份落 <projectRoot>/.sillyspec/.runtime/
|
|
140
|
+
// merge-backups/(运行时目录,doctor/hygiene 常规清理面,不污染工作区)。
|
|
141
|
+
try {
|
|
142
|
+
const backupDir = join(projectRoot, '.sillyspec', '.runtime', 'merge-backups')
|
|
143
|
+
mkdirSync(backupDir, { recursive: true })
|
|
144
|
+
writeFileSync(join(backupDir, `${Date.now()}-${f.replace(/[\\/]/g, '__')}`), mainRaw)
|
|
145
|
+
} catch { /* 备份失败不阻断合并(原文仍可经 rescue 出口重取,不静默弃救 */ }
|
|
146
|
+
writeFileSync(join(projectRoot, f), outBuf)
|
|
147
|
+
merged.push(f)
|
|
148
|
+
console.log(`🔀 EXCLUDE-DIRTY 三方合并成功(clean): ${f}——主仓在途改动与 worktree 交付均已保留(覆写前原文已备份至 .sillyspec/.runtime/merge-backups/)`)
|
|
149
|
+
} else {
|
|
150
|
+
conflicts.push(f)
|
|
151
|
+
}
|
|
152
|
+
} catch { conflicts.push(f) }
|
|
153
|
+
}
|
|
154
|
+
} finally {
|
|
155
|
+
try { rmSync(tmpDir, { recursive: true, force: true }) } catch {}
|
|
156
|
+
}
|
|
157
|
+
// 批末统一暂存(task-01 / FR-01 / D-001@v1 前半,2026-09-14-apply-conflict-hardening):
|
|
158
|
+
// 本写回点曾是全链路唯一未暂存的自动写点——未暂存文件对主仓工作区级 git 操作
|
|
159
|
+
// (restore / clean / checkout)零免疫(§64 事故的实际缺口)。对全部 clean 写回文件
|
|
160
|
+
// 显式 pathspec git add(safeGit 数组形式不经 shell;chunkPaths 分批防 Windows argv
|
|
161
|
+
// 上限,批 D-③ 同款)。fail-open:add 失败不抛(合并成果保留),console.warn 留痕 +
|
|
162
|
+
// 返回 stagedOk:false——manifest 侧对该批文件自动回退 readFile+归一 approx 口径。
|
|
163
|
+
let stagedOk = true
|
|
164
|
+
if (merged.length > 0) {
|
|
165
|
+
for (const batch of chunkPaths(merged)) {
|
|
166
|
+
const r = safeGit(projectRoot, ['add', '--', ...batch])
|
|
167
|
+
if (r && r.error) {
|
|
168
|
+
stagedOk = false
|
|
169
|
+
console.warn(`⚠️ merge 写回批末 git add 失败(fail-open:${merged.length} 个合并文件成果保留,但未暂存对 git restore/clean 不免疫,请尽快人工 git add):${String(r.error).split('\n')[0]}`)
|
|
170
|
+
break
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return { merged, conflicts, stagedOk }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* 字节级 CRLF→LF 归一(ql-20260915-001 修复①,mergeMismatchThreeWay 专用)。
|
|
179
|
+
*
|
|
180
|
+
* 动机:本仓 core.autocrlf=true(2026-09-15 实测),git checkout 出的 worktree 磁盘内容是
|
|
181
|
+
* CRLF、blob(git show 取出)是 LF——ours(blob LF) 与 theirs(disk CRLF) 混形喂 merge-file 会
|
|
182
|
+
* 逐行假冲突(每一行都呈现为两侧各改一次)。三输入统一 LF 域合并,写回后 git add 按
|
|
183
|
+
* autocrlf 属性归一回 blob 域(磁盘 LF 形态 git status 亦不报幻影改动)。
|
|
184
|
+
*
|
|
185
|
+
* 字节级替换(latin1 语义,不经 utf8 解码)保任意非 UTF-8 文本字节不被替换字符污染;含 NUL
|
|
186
|
+
* (二进制标志)返回 null——二进制不可文本合并,调用方按冲突处理。
|
|
187
|
+
*
|
|
188
|
+
* @param {Buffer} buf
|
|
189
|
+
* @returns {Buffer|null} 归一后内容;二进制返回 null
|
|
190
|
+
*/
|
|
191
|
+
function _toLfBuffer(buf) {
|
|
192
|
+
if (!buf || buf.length === 0) return buf
|
|
193
|
+
if (buf.indexOf(0) !== -1) return null // 二进制
|
|
194
|
+
if (buf.indexOf(Buffer.from('\r\n')) === -1) return buf
|
|
195
|
+
const out = []
|
|
196
|
+
for (let i = 0; i < buf.length; i++) {
|
|
197
|
+
if (buf[i] === 0x0d && i + 1 < buf.length && buf[i + 1] === 0x0a) { out.push(0x0a); i++ }
|
|
198
|
+
else out.push(buf[i])
|
|
199
|
+
}
|
|
200
|
+
return Buffer.from(out)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* EXCLUDE-MISMATCH 前置自动三方合并(ql-20260915-001 修复①,坑 apply-archived-mismatch-no-merge:
|
|
205
|
+
* 2026-09-14 用户实证归档后补 apply 时主仓文件已被并行会话提交推进 → rescue EXCLUDE-MISMATCH
|
|
206
|
+
* 跳过不落地,被迫手工 cp + 锚点合并)。
|
|
207
|
+
*
|
|
208
|
+
* 与 mergeDirtyOverlapThreeWay 同实现形态,base/ours 取源不同:
|
|
209
|
+
* - base = apply 锚点(baseHash 形参,调用方传 deliverableBase=baselineCommit||baseHash)blob
|
|
210
|
+
* - **ours = 主仓 HEAD blob(已提交推进态,非脏读——mismatch 面主仓无未提交改动,
|
|
211
|
+
* 调用方已按 pre-stash dirty 集排除,HEAD 即主仓当前态)**
|
|
212
|
+
* - theirs = worktree 文件内容(磁盘读,子代理未 commit 的交付)
|
|
213
|
+
*
|
|
214
|
+
* clean 合并才写回主仓(两侧增量都保留)+ 批末 git add(chunkPaths 分批)+ 调用方从
|
|
215
|
+
* changedFiles/hashMismatchFiles 剔除(patch 不再重放,防 staged 内容与 patch 预像互踩);
|
|
216
|
+
* conflict / 异常 fail-soft 维持原路径(rescue EXCLUDE-MISMATCH 新文案 / step7 --3way 实测)。
|
|
217
|
+
* 覆写前备份主仓磁盘原文至 .sillyspec/.runtime/merge-backups/(与脏读版同一惯例)。
|
|
218
|
+
*
|
|
219
|
+
* @param {{ projectRoot: string, worktreePath: string, baseHash: string }} ctx
|
|
220
|
+
* @param {string[]} mismatchFiles mismatch ∩ changedFiles 且主仓无未提交改动的文件(仓库根相对路径)
|
|
221
|
+
* @returns {{ merged: string[], conflicts: string[], stagedOk: boolean }}
|
|
222
|
+
*/
|
|
223
|
+
export function mergeMismatchThreeWay({ projectRoot, worktreePath, baseHash }, mismatchFiles) {
|
|
224
|
+
const merged = []
|
|
225
|
+
const conflicts = []
|
|
226
|
+
const tmpDir = mkdtempSync(join(tmpdir(), 'sillyspec-3way-m-'))
|
|
227
|
+
try {
|
|
228
|
+
for (const f of mismatchFiles) {
|
|
229
|
+
try {
|
|
230
|
+
// ours/HEAD 缺失(主干侧删除等 modify/delete 形态)→ git show 抛错 → conflicts(fail-soft,
|
|
231
|
+
// 删除语义 merge-file 无法表达,交既有出口 --3way/--merge 裁决)
|
|
232
|
+
const baseRaw = git(projectRoot, ['show', `${baseHash}:${f}`], { encoding: 'buffer', timeout: 15000 })
|
|
233
|
+
const oursRaw = git(projectRoot, ['show', `HEAD:${f}`], { encoding: 'buffer', timeout: 15000 })
|
|
234
|
+
const wtRaw = readFileSync(join(worktreePath, f))
|
|
235
|
+
const baseN = _toLfBuffer(baseRaw)
|
|
236
|
+
const oursN = _toLfBuffer(oursRaw)
|
|
237
|
+
const theirsN = _toLfBuffer(wtRaw)
|
|
238
|
+
if (!baseN || !oursN || !theirsN) { conflicts.push(f); continue } // 二进制 → 原路径
|
|
239
|
+
const p = (tag) => join(tmpDir, `${tag}-${Math.random().toString(36).slice(2, 8)}`)
|
|
240
|
+
const baseP = p('base'); const oursP = p('ours'); const theirsP = p('theirs')
|
|
241
|
+
writeFileSync(baseP, baseN); writeFileSync(oursP, oursN); writeFileSync(theirsP, theirsN)
|
|
242
|
+
let outBuf, code = 0
|
|
243
|
+
try {
|
|
244
|
+
outBuf = git(projectRoot, ['merge-file', '-p', oursP, baseP, theirsP], { encoding: 'buffer', timeout: 15000 })
|
|
245
|
+
} catch (e) {
|
|
246
|
+
code = (typeof e.status === 'number') ? e.status : -1
|
|
247
|
+
outBuf = (e.stdout instanceof Buffer) ? e.stdout : Buffer.alloc(0)
|
|
248
|
+
if (code < 0) { conflicts.push(f); continue }
|
|
249
|
+
}
|
|
250
|
+
if (code === 0) {
|
|
251
|
+
// 覆写前备份主仓磁盘原文(merge-backups 惯例;ours=HEAD 理论可经 git show 重取,仍留备份保审计)
|
|
252
|
+
try {
|
|
253
|
+
const backupDir = join(projectRoot, '.sillyspec', '.runtime', 'merge-backups')
|
|
254
|
+
mkdirSync(backupDir, { recursive: true })
|
|
255
|
+
let pre = oursRaw
|
|
256
|
+
try { pre = readFileSync(join(projectRoot, f)) } catch { /* 磁盘缺文件用 HEAD 原文 */ }
|
|
257
|
+
writeFileSync(join(backupDir, `${Date.now()}-${f.replace(/[\\/]/g, '__')}`), pre)
|
|
258
|
+
} catch { /* 备份失败不阻断合并 */ }
|
|
134
259
|
writeFileSync(join(projectRoot, f), outBuf)
|
|
135
260
|
merged.push(f)
|
|
136
|
-
console.log(`🔀 EXCLUDE-
|
|
261
|
+
console.log(`🔀 EXCLUDE-MISMATCH 三方合并成功(clean): ${f}——主仓已提交推进与 worktree 交付均已保留(覆写前原文已备份至 .sillyspec/.runtime/merge-backups/)`)
|
|
137
262
|
} else {
|
|
138
263
|
conflicts.push(f)
|
|
139
264
|
}
|
|
@@ -142,7 +267,20 @@ export function mergeDirtyOverlapThreeWay({ projectRoot, worktreePath, baseHash
|
|
|
142
267
|
} finally {
|
|
143
268
|
try { rmSync(tmpDir, { recursive: true, force: true }) } catch {}
|
|
144
269
|
}
|
|
145
|
-
|
|
270
|
+
// 批末统一暂存(与 mergeDirtyOverlapThreeWay 同款:safeGit 数组形式 + chunkPaths 分批,
|
|
271
|
+
// fail-open:add 失败不抛、stagedOk:false 供调用方 warning 提示人工补 add)
|
|
272
|
+
let stagedOk = true
|
|
273
|
+
if (merged.length > 0) {
|
|
274
|
+
for (const batch of chunkPaths(merged)) {
|
|
275
|
+
const r = safeGit(projectRoot, ['add', '--', ...batch])
|
|
276
|
+
if (r && r.error) {
|
|
277
|
+
stagedOk = false
|
|
278
|
+
console.warn(`⚠️ mismatch merge 写回批末 git add 失败(fail-open:${merged.length} 个合并文件成果保留,但未暂存对 git restore/clean 不免疫,请尽快人工 git add):${String(r.error).split('\n')[0]}`)
|
|
279
|
+
break
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return { merged, conflicts, stagedOk }
|
|
146
284
|
}
|
|
147
285
|
|
|
148
286
|
export function generateRescueCommands({ changedFiles, dirtyFiles, hashMismatchFiles, deletedFiles = [], worktreePath, projectRoot }) {
|
|
@@ -168,7 +306,9 @@ export function generateRescueCommands({ changedFiles, dirtyFiles, hashMismatchF
|
|
|
168
306
|
continue;
|
|
169
307
|
}
|
|
170
308
|
if (mismatchSet.has(f)) {
|
|
171
|
-
|
|
309
|
+
// ql-20260915-001 修复①:文案更新——前置三方合并已尝试过(4.5b),此处剩的是合并冲突面,
|
|
310
|
+
// 给出两条实际出路(手动锚点合并 / 先 commit 后 --3way),不再误导「正常 apply 走 --3way」即可解
|
|
311
|
+
warnings.push(`跳过 ${f}:EXCLUDE-MISMATCH(主干已提交推进该文件,已尝试自动三方合并,冲突——手动锚点合并,或先 commit 主仓未提交改动后正常 apply 走 --3way)`);
|
|
172
312
|
excludedCount++;
|
|
173
313
|
continue;
|
|
174
314
|
}
|
|
@@ -176,6 +316,15 @@ export function generateRescueCommands({ changedFiles, dirtyFiles, hashMismatchF
|
|
|
176
316
|
cpFileCount++;
|
|
177
317
|
}
|
|
178
318
|
|
|
319
|
+
// D-003@v1(task-01,2026-09-14-apply-conflict-hardening):rescue 是人工落地路径,落地文件
|
|
320
|
+
// 不进 manifest 也不自动暂存——主仓工作区级 git 操作(restore/clean/checkout)可静默冲掉
|
|
321
|
+
// 未暂存落地(§64 事故实际路径)。commands 末尾追加一行「落地后立即 git add 锁定」指引
|
|
322
|
+
// (# 注释形态,整块复制粘贴仍是合法 shell;纯文案,commands/warnings 结构语义与
|
|
323
|
+
// cpFileCount/excludedCount 口径不变;commands 为空 = 无可落地文件,不加指引)。
|
|
324
|
+
if (commands.length > 0) {
|
|
325
|
+
commands.push(`# 全部执行后立即 git add -- <上方 cp 落地的各文件路径> 锁定暂存区(staged 对 git restore / git clean 免疫,防止并行会话的工作区级操作冲掉刚落地的文件;rm 删除的文件无需 add)`);
|
|
326
|
+
}
|
|
327
|
+
|
|
179
328
|
return { commands, warnings, cpFileCount, excludedCount };
|
|
180
329
|
}
|
|
181
330
|
|
|
@@ -242,19 +391,39 @@ export function resolvePatchFiles(changedFiles, allowSet, hasAllowList) {
|
|
|
242
391
|
function getBlobHashMap(cwd, treeish, files) {
|
|
243
392
|
const map = new Map();
|
|
244
393
|
if (files.length === 0) return map; // 空 pathspec 会让 ls-tree 列出整棵树,必须拦截
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
394
|
+
for (const batch of chunkPaths(files)) {
|
|
395
|
+
const raw = gitQuiet(cwd, ['ls-tree', treeish, '--', ...batch]);
|
|
396
|
+
if (!raw) continue
|
|
397
|
+
for (const line of raw.split('\n')) {
|
|
398
|
+
if (!line) continue;
|
|
399
|
+
const tabIdx = line.indexOf('\t');
|
|
400
|
+
if (tabIdx === -1) continue;
|
|
401
|
+
const filePath = line.slice(tabIdx + 1);
|
|
402
|
+
const hash = line.slice(0, tabIdx).split(' ')[2]; // "<mode> <type> <hash>"
|
|
403
|
+
if (hash) map.set(filePath, hash);
|
|
404
|
+
}
|
|
254
405
|
}
|
|
255
406
|
return map;
|
|
256
407
|
}
|
|
257
408
|
|
|
409
|
+
/**
|
|
410
|
+
* 按估算 argv 长度切批(2026-09-12 审查批 D-③):Windows CreateProcess 命令行 32767 字符
|
|
411
|
+
* 上限——数百个长路径(Windows 深路径 160+ 字符 × 200 文件)全量展开时 spawn 直接失败,
|
|
412
|
+
* add/reset/diff 整体炸(hash-object 同病灶在 worktree.js,本文件先行收口)。保守 8000
|
|
413
|
+
* 字符/批,预留 git 参数与编码余量。返回至少一批(空输入返回单空批)。
|
|
414
|
+
*/
|
|
415
|
+
export function chunkPaths(paths, maxChars = 8000) {
|
|
416
|
+
const batches = [[]]
|
|
417
|
+
let len = 0
|
|
418
|
+
for (const p of paths) {
|
|
419
|
+
const l = String(p).length + 1
|
|
420
|
+
if (batches[batches.length - 1].length > 0 && len + l > maxChars) { batches.push([]); len = 0 }
|
|
421
|
+
batches[batches.length - 1].push(p)
|
|
422
|
+
len += l
|
|
423
|
+
}
|
|
424
|
+
return batches
|
|
425
|
+
}
|
|
426
|
+
|
|
258
427
|
/**
|
|
259
428
|
* apply 的文件清单 = design §6 清单 ∪ plan TaskCard allowed_paths(execute 复盘 c)。
|
|
260
429
|
*
|
|
@@ -345,14 +514,127 @@ function _silentPointerSpecRoot(projectRoot) {
|
|
|
345
514
|
} catch { return null; }
|
|
346
515
|
}
|
|
347
516
|
|
|
517
|
+
/**
|
|
518
|
+
* apply 成功尾声写 apply-manifest.json(task-01 / FR-02 / D-001@v1 后半,
|
|
519
|
+
* 2026-09-14-apply-conflict-hardening)。
|
|
520
|
+
*
|
|
521
|
+
* 目的:apply 后交付文件在主仓被并行会话的工作区级 git 操作冲掉(新文件删除/修改回退)时
|
|
522
|
+
* 靠指纹可检测(doctor 漂移检查项消费,Wave 3)。CLI 全权写(agent 勿手改,verify-facts
|
|
523
|
+
* 同款契约);rescue 人工落地文件不进 manifest(design 非目标边界)。
|
|
524
|
+
*
|
|
525
|
+
* 哈希口径(design 接口定义定案,AGENTS.md 规则 13 Windows 兼容):**staged blob 内容**
|
|
526
|
+
* (git show :<path>,autocrlf 归一后的 LF 规范态)——写侧在 git add / apply --3way(隐含
|
|
527
|
+
* --index)之后取值,与 doctor 的 staged 态比对天然同基。文件未进暂存(如 merge 写回批末
|
|
528
|
+
* add 失败的 fail-open 面)→ 回退 readFile 后 CRLF→LF 归一计算(latin1 往返保字节),条目
|
|
529
|
+
* 加 approx:true 标记;staged 与磁盘皆无(apply 删除的文件——无内容可指纹)→ 不进 manifest。
|
|
530
|
+
*
|
|
531
|
+
* 落点:<changeDir>/apply-manifest.json(resolveActiveOrArchiveChangeDir 归档感知——与
|
|
532
|
+
* resolveApplyAllowSet 同源,归档后补 apply 的重放写在归档目录)。已存在则覆盖(重放 apply
|
|
533
|
+
* 以最新为准)。manifest 落 .sillyspec/changes/ 面 → filterDeliverableFiles 排除,不构成
|
|
534
|
+
* apply 交付物、不进自身 files。
|
|
535
|
+
*
|
|
536
|
+
* 本函数自身不 catch 写失败(交调用方 fail-open warning);files 为空时跳过不写(无落盘面
|
|
537
|
+
* 的成功出口不产 manifest,doctor 按存在性跳过)。
|
|
538
|
+
*
|
|
539
|
+
* @param {{ projectRoot: string, specBase?: string, changeName: string, baseHash?: string, files: string[] }} args
|
|
540
|
+
* specBase 缺省时与 resolveApplyAllowSet 同链回退(指针 specRoot > 本地 .sillyspec);
|
|
541
|
+
* baseHash 记 apply 锚点(deliverableBase 语义:baselineCommit || baseHash)
|
|
542
|
+
* @returns {{ written: boolean, file?: string, files: Array<{path:string, sha256:string, approx?:true}>, skipped?: string }}
|
|
543
|
+
*/
|
|
544
|
+
function writeApplyManifest({ projectRoot, specBase, changeName, baseHash, files }) {
|
|
545
|
+
const face = [...new Set((Array.isArray(files) ? files : []).filter(Boolean))];
|
|
546
|
+
if (face.length === 0) return { written: false, files: [], skipped: 'empty-files' };
|
|
547
|
+
const base = specBase || _silentPointerSpecRoot(projectRoot) || join(projectRoot, '.sillyspec');
|
|
548
|
+
const changeDir = resolveActiveOrArchiveChangeDir(base, changeName);
|
|
549
|
+
const entries = [];
|
|
550
|
+
for (const f of face) {
|
|
551
|
+
let sha256 = null;
|
|
552
|
+
let approx = false;
|
|
553
|
+
try {
|
|
554
|
+
// staged blob(LF 规范态):encoding buffer 保二进制原样;不在 index(删除/未 add)→ 抛错走回退
|
|
555
|
+
const blob = git(projectRoot, ['show', `:${f}`], { encoding: 'buffer', timeout: 15000 });
|
|
556
|
+
sha256 = createHash('sha256').update(blob).digest('hex');
|
|
557
|
+
} catch {
|
|
558
|
+
try {
|
|
559
|
+
const raw = readFileSync(join(projectRoot, f));
|
|
560
|
+
// CRLF→LF 归一(latin1 往返保字节,防 utf8 解码破坏二进制内容)
|
|
561
|
+
const norm = Buffer.from(raw.toString('latin1').replace(/\r\n/g, '\n'), 'latin1');
|
|
562
|
+
sha256 = createHash('sha256').update(norm).digest('hex');
|
|
563
|
+
approx = true;
|
|
564
|
+
} catch { continue; } // staged 与磁盘皆无(apply 删除面)→ 无内容可指纹,不进 manifest
|
|
565
|
+
}
|
|
566
|
+
entries.push(approx ? { path: f, sha256, approx: true } : { path: f, sha256 });
|
|
567
|
+
}
|
|
568
|
+
const manifest = {
|
|
569
|
+
schemaVersion: 1,
|
|
570
|
+
change: changeName,
|
|
571
|
+
appliedAt: new Date().toISOString(),
|
|
572
|
+
baseHash: baseHash || null,
|
|
573
|
+
files: entries,
|
|
574
|
+
};
|
|
575
|
+
mkdirSync(changeDir, { recursive: true });
|
|
576
|
+
const manifestPath = join(changeDir, 'apply-manifest.json');
|
|
577
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');
|
|
578
|
+
return { written: true, file: manifestPath, files: entries };
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* 坑 mixed-baseline-drift-hint(ql-20260915-004):apply 成功尾声混合基线 advisory。
|
|
583
|
+
*
|
|
584
|
+
* 主仓自本变更基点(baseHash)后的已提交推进若触及本变更相关文件(含 .test./.spec. 变体),
|
|
585
|
+
* worktree 快照内验证过的测试断言可能基于旧内容——apply 落地的是混合基线,提示 agent 合并态
|
|
586
|
+
* 复跑相关测试(非阻断,result.committedDrift 留痕)。head 缺省 HEAD:patch 路径自身交付
|
|
587
|
+
* staged 未提交,baseHash..HEAD 即纯他者推进面;merge 路径传 preMergeHead(merge 提交后
|
|
588
|
+
* HEAD 含自身交付,不剔会把「自己合自己」误报成他者推进)。fail-soft:异常零影响 apply。
|
|
589
|
+
*/
|
|
590
|
+
function annotateCommittedDrift(result, { projectRoot, baseHash, files, head }) {
|
|
591
|
+
try {
|
|
592
|
+
if (!baseHash || !files || files.length === 0) return;
|
|
593
|
+
const drift = detectCommittedDrift({ projectRoot, baseHash, touchedFiles: files, head });
|
|
594
|
+
if (drift && drift.drift) {
|
|
595
|
+
result.committedDrift = drift;
|
|
596
|
+
console.warn(formatCommittedDriftWarning(drift));
|
|
597
|
+
}
|
|
598
|
+
} catch { /* fail-soft:drift 检测异常不影响 apply 结果 */ }
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* guard 相交预检(task-02 / FR-03 / D-002@v1,内部函数)。
|
|
603
|
+
*
|
|
604
|
+
* 活跃 quick 会话 guard.allowedFiles 与本次 apply 实际落盘集求交集——非空说明本变更文件正被
|
|
605
|
+
* 并行 quick 会话声明在途改动,此刻 apply 会互冲(2026-09-14 实证事故:apply 交付文件被并行
|
|
606
|
+
* 会话工作区 git 操作冲掉)。交集口径:字符串等值(guard.allowedFiles 与 changedFiles 同为
|
|
607
|
+
* 仓库根相对路径)。无活跃会话/无声明/交集空 → blocked=false(零行为变化)。
|
|
608
|
+
*
|
|
609
|
+
* @param {{ activeGuards: Map<string, string[]>, applyFiles: string[], selfChange?: string }} p
|
|
610
|
+
* - activeGuards:collectActiveQuickGuardFiles 产物(已按 excludeChange 排除自身)
|
|
611
|
+
* - applyFiles:changedFiles∪newPatchFiles(result 实际字段口径)
|
|
612
|
+
* - selfChange:防御性再排除(sessionId == 本次 changeName 不拦自身)
|
|
613
|
+
* @returns {{ overlaps: Array<{sessionId: string, file: string}>, blocked: boolean }}
|
|
614
|
+
*/
|
|
615
|
+
function checkGuardOverlap({ activeGuards, applyFiles, selfChange }) {
|
|
616
|
+
const overlaps = [];
|
|
617
|
+
const fileSet = new Set((applyFiles || []).filter(Boolean));
|
|
618
|
+
if (!activeGuards || fileSet.size === 0) return { overlaps, blocked: false };
|
|
619
|
+
for (const [sessionId, allowedFiles] of activeGuards) {
|
|
620
|
+
if (selfChange && sessionId === selfChange) continue;
|
|
621
|
+
for (const f of (allowedFiles || [])) {
|
|
622
|
+
if (fileSet.has(f)) overlaps.push({ sessionId, file: f });
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
return { overlaps, blocked: overlaps.length > 0 };
|
|
626
|
+
}
|
|
627
|
+
|
|
348
628
|
/**
|
|
349
629
|
* 收集最新 execute run 各 task review.json 的 changedFiles 声明 → Map<repoKey, string[]>。
|
|
350
630
|
*
|
|
351
631
|
* 坑 apply-undeclared-deviation-block(2026-08-24 用户反馈四期③):执行期有据越界文件
|
|
352
632
|
* (facade 转发/名单测试)不在 design §6 也不在 allowed_paths,Gate1 拦 apply 只能回改
|
|
353
633
|
* design.md。review.json changedFiles 是 reviewer 对实际改动的声明(Task Review Gate 的
|
|
354
|
-
* verifyReviewGitEvidence 已交叉校验其与真实 git diff
|
|
355
|
-
*
|
|
634
|
+
* verifyReviewGitEvidence 已交叉校验其与真实 git diff 相交)。D-003@v1 收紧(2026-09-14
|
|
635
|
+
* change-ownership-guards task-03):声明只与 allow 面相交(pathMatches 容差)才计入,
|
|
636
|
+
* 不相交的外来声明剔除出放行面、进 violations 报告行——本函数仍是声明收集口(口径不变),
|
|
637
|
+
* 相交过滤在主仓 3b/跨仓清单校验两消费点执行。口径与 complete-handlers 的
|
|
356
638
|
* collectExecuteChangedFiles 一致:change 戳归属 run、.sillyspec//meta.json 过滤、
|
|
357
639
|
* review.repo 切片(跨仓声明不进 main 集)。读不到 run/review → 空 Map(fail-closed 回退旧行为)。
|
|
358
640
|
*
|
|
@@ -481,9 +763,12 @@ function validateCrossRepoNoOp(ctx, projectRoot, changeName) {
|
|
|
481
763
|
* 可清,wm.cleanup 只作用于主仓 worktree)。主仓 task 走原 A5 完整 apply 不动(GOAL-2 单仓零回归)。
|
|
482
764
|
*
|
|
483
765
|
* @param {string} changeName - 变更名
|
|
484
|
-
* @param {{ cwd?: string, checkOnly?: boolean, merge?: boolean, ctx?: object }} opts
|
|
766
|
+
* @param {{ cwd?: string, checkOnly?: boolean, merge?: boolean, ctx?: object, force?: boolean, autoApply?: boolean }} opts
|
|
485
767
|
* - ctx:可选 MultiRepoContext(design §7.1)。缺省=单仓退化(仅主仓 apply,零行为变化,GOAL-2)。
|
|
486
768
|
* 提供 ctx 且含跨仓 entry 时,触发跨仓 no-op 校验(校验 review.head 真实 + 不 cleanup 跨仓)。
|
|
769
|
+
* - force(task-02 / FR-03):guard 相交预检命中时显式放行并留痕 result.overlapForced(仅人工 CLI --force)。
|
|
770
|
+
* - autoApply(task-02 / FR-03):assess 自动入口置 true——相交命中软跳过(result.overlapSkipped + warning,
|
|
771
|
+
* 不抛错不落盘);自动路径永不 force。
|
|
487
772
|
* @returns {{
|
|
488
773
|
* ok: boolean,
|
|
489
774
|
* changedFiles: string[],
|
|
@@ -554,7 +839,7 @@ export async function withMainRepoLock(projectRoot, changeName, purpose, fn, opt
|
|
|
554
839
|
*
|
|
555
840
|
* 对有 worktree meta 的跨仓仓(worktree-cross.js 创建)执行与主仓 A5 同构的 patch 回落:
|
|
556
841
|
* deliverables(diff baselineCommit||baseHash + untracked,filterDeliverableFiles 排除基础设施)
|
|
557
|
-
* → 清单校验(resolveApplyAllowSet 按 repo 切片
|
|
842
|
+
* → 清单校验(resolveApplyAllowSet 按 repo 切片 + review 声明相交过滤,D-003:不相交剔除)
|
|
558
843
|
* → 跨仓主工作副本 dirty 重叠拦截
|
|
559
844
|
* → patch(tracked diff + untracked add/diff --cached/reset,Buffer 防二进制损坏)
|
|
560
845
|
* → git apply --3way 回跨仓主工作副本;成功后 cleanup 该跨仓 worktree(分支保留作锚)。
|
|
@@ -612,24 +897,45 @@ function applyCrossRepoWorktrees(changeName, projectRoot, ctx, { checkOnly = fal
|
|
|
612
897
|
continue;
|
|
613
898
|
}
|
|
614
899
|
|
|
615
|
-
// 清单校验(per-repo 切片 + review
|
|
900
|
+
// 清单校验(per-repo 切片 + review 声明相交过滤,与主仓 Gate1/3b 同语义)。
|
|
616
901
|
// specBase 显式传(平台模式 changes/ 在 specRoot;本地模式与旧硬编码同值零回归)。
|
|
902
|
+
// review 声明收紧(D-003@v1,2026-09-14-change-ownership-guards task-03):声明文件须与
|
|
903
|
+
// allow 面(design 清单 ∪ 各 task target_files/allowed_paths ∪ linked-change 声明 =
|
|
904
|
+
// resolveApplyAllowSet 现有并集)相交(pathMatches 容差)才计入;不相交的外来声明剔除出
|
|
905
|
+
// 放行面、进 violations 报告行(「review 声明了越权文件」嫌疑标注),admission 增量归零。
|
|
617
906
|
const allowMap = resolveApplyAllowSet(projectRoot, changeName, { specBase });
|
|
618
|
-
|
|
907
|
+
const allowSet = allowMap.get(repoKey) || new Set();
|
|
619
908
|
const reviewDeclared = collectReviewDeclaredFiles(projectRoot, changeName, { runtimeRoot: join(specBase, '.runtime') }).get(repoKey);
|
|
620
|
-
|
|
621
|
-
|
|
909
|
+
const reviewForeign = [];
|
|
910
|
+
if (reviewDeclared && allowSet.size > 0) {
|
|
911
|
+
const face = [...allowSet];
|
|
622
912
|
for (const f of reviewDeclared) {
|
|
623
|
-
if (!
|
|
913
|
+
if (!face.some(ap => pathMatches(f, ap))) reviewForeign.push(f);
|
|
624
914
|
}
|
|
625
|
-
|
|
915
|
+
}
|
|
916
|
+
if (reviewForeign.length > 0) {
|
|
917
|
+
out.warnings.push(
|
|
918
|
+
`跨仓 ${repoKey}:${reviewForeign.length} 个 review 声明文件不在 allow 面(design 清单/任务卡 allowed_paths/linked 声明),已剔除出放行面(review 声明了越权文件——嫌疑:review 失实或并行会话在途文件混入声明):${reviewForeign.join(', ')}`
|
|
919
|
+
);
|
|
626
920
|
}
|
|
627
921
|
if (allowSet.size > 0) {
|
|
628
922
|
const violations = classifyAllowListViolations(changedFiles, allowSet);
|
|
629
923
|
if (violations.length > 0) {
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
);
|
|
924
|
+
const foreignSet = new Set(reviewForeign);
|
|
925
|
+
const foreignViolations = violations.filter(f => foreignSet.has(f));
|
|
926
|
+
const undeclaredViolations = violations.filter(f => !foreignSet.has(f));
|
|
927
|
+
const lines = [];
|
|
928
|
+
if (undeclaredViolations.length > 0) {
|
|
929
|
+
lines.push(
|
|
930
|
+
`跨仓 ${repoKey}:文件清单校验失败——以下变更文件不在 design.md 清单、也不在该仓 task review.json changedFiles 声明中:\n ${undeclaredViolations.join('\n ')}`
|
|
931
|
+
);
|
|
932
|
+
}
|
|
933
|
+
if (foreignViolations.length > 0) {
|
|
934
|
+
lines.push(
|
|
935
|
+
`跨仓 ${repoKey}:review 声明了越权文件——以下变更文件已被 review.json changedFiles 声明但不在 allow 面(design 清单/任务卡 allowed_paths/linked 声明),不予放行:\n ${foreignViolations.join('\n ')}\n(确属本变更交付:补进 design §6 清单或任务卡 allowed_paths 后重跑)`
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
out.errors.push(lines.join('\n'));
|
|
633
939
|
if (!checkOnly) continue;
|
|
634
940
|
}
|
|
635
941
|
}
|
|
@@ -676,14 +982,24 @@ function applyCrossRepoWorktrees(changeName, projectRoot, ctx, { checkOnly = fal
|
|
|
676
982
|
const untrackedPatchFiles = patchFiles.filter(f => !trackedFiles.includes(f));
|
|
677
983
|
const patchParts = [];
|
|
678
984
|
if (trackedFiles.length > 0) {
|
|
679
|
-
|
|
985
|
+
// argv 分批(批 D-③):数百长路径全量展开超 Windows 命令行上限;diff 输出按批拼接
|
|
986
|
+
// (各批均为完整 patch 文档,concat 对 git apply 合法)
|
|
987
|
+
for (const batch of chunkPaths(trackedFiles)) {
|
|
988
|
+
patchParts.push(git(cm.worktreePath, ['diff', '--binary', deliverableBase, '--', ...batch], { encoding: 'buffer', timeout: 60000 }));
|
|
989
|
+
}
|
|
680
990
|
}
|
|
681
991
|
if (untrackedPatchFiles.length > 0) {
|
|
682
|
-
|
|
992
|
+
for (const batch of chunkPaths(untrackedPatchFiles)) {
|
|
993
|
+
git(cm.worktreePath, ['add', '--', ...batch]);
|
|
994
|
+
}
|
|
683
995
|
try {
|
|
684
|
-
|
|
996
|
+
for (const batch of chunkPaths(untrackedPatchFiles)) {
|
|
997
|
+
patchParts.push(git(cm.worktreePath, ['diff', '--binary', '--cached', '--', ...batch], { encoding: 'buffer', timeout: 60000 }));
|
|
998
|
+
}
|
|
685
999
|
} finally {
|
|
686
|
-
|
|
1000
|
+
for (const batch of chunkPaths(untrackedPatchFiles)) {
|
|
1001
|
+
gitQuiet(cm.worktreePath, ['reset', 'HEAD', '--', ...batch]);
|
|
1002
|
+
}
|
|
687
1003
|
}
|
|
688
1004
|
}
|
|
689
1005
|
const patchContent = Buffer.concat(patchParts);
|
|
@@ -726,7 +1042,7 @@ function applyCrossRepoWorktrees(changeName, projectRoot, ctx, { checkOnly = fal
|
|
|
726
1042
|
return out;
|
|
727
1043
|
}
|
|
728
1044
|
|
|
729
|
-
export function applyWorktree(changeName, { cwd, checkOnly = false, merge = false, base = 'merge-base', ctx = null, skipOverlap = false, stashDirty = false } = {}) {
|
|
1045
|
+
export function applyWorktree(changeName, { cwd, checkOnly = false, merge = false, base = 'merge-base', ctx = null, skipOverlap = false, stashDirty = false, force = false, autoApply = false } = {}) {
|
|
730
1046
|
const projectRoot = cwd || process.cwd();
|
|
731
1047
|
const wm = new WorktreeManager({ cwd: projectRoot });
|
|
732
1048
|
const meta = wm.getMeta(changeName);
|
|
@@ -873,27 +1189,30 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
|
|
|
873
1189
|
// (主仓 worktree diff 只含主仓文件,跨仓文件在跨仓仓不进主仓 worktree diff)。跨仓 allowed_paths
|
|
874
1190
|
// 仅作 Map 切片返回供上游用,主流程不消费(跨仓 apply=no-op,D-009)。
|
|
875
1191
|
const allowMap = resolveApplyAllowSet(projectRoot, changeName, { specBase: allowSpecBase });
|
|
876
|
-
// --- 3b. review.json
|
|
877
|
-
//
|
|
878
|
-
//
|
|
879
|
-
//
|
|
1192
|
+
// --- 3b. review.json 声明偏差文件相交过滤(坑 apply-undeclared-deviation-block → D-003@v1 收紧,
|
|
1193
|
+
// 2026-09-14-change-ownership-guards task-03 / FR-02)---
|
|
1194
|
+
// 旧语义:reviewer 声明的 changedFiles 整体并入 allow set(声明即放行)——troubleshooting §65
|
|
1195
|
+
// 事件②实证该通道无 allowed_paths 相交校验,11 个并行会话在途文件经 review 声明真实放行。
|
|
1196
|
+
// 新语义(D-003 翻转):review 声明只承认与 allow 面(design 清单 ∪ 各 task
|
|
1197
|
+
// target_files/allowed_paths ∪ linked-change 声明 = resolveApplyAllowSet 现有并集)相交的文件
|
|
1198
|
+
// (pathMatches 容差);不相交的外来声明剔除出放行面、进 violations 报告行(「review 声明了
|
|
1199
|
+
// 越权文件」嫌疑标注)——admission 增量归零,通道保留作审计报告位(外来声明显式列出)。
|
|
880
1200
|
const reviewDeclaredByRepo = collectReviewDeclaredFiles(projectRoot, changeName, { runtimeRoot: allowRuntimeRoot });
|
|
881
|
-
const
|
|
1201
|
+
const reviewForeignMain = [];
|
|
882
1202
|
for (const [repoKey, files] of reviewDeclaredByRepo) {
|
|
883
1203
|
const repoSet = allowMap.get(repoKey);
|
|
884
|
-
if (!repoSet) continue; // 该 repo 无 design
|
|
885
|
-
const
|
|
1204
|
+
if (!repoSet || repoSet.size === 0) continue; // 该 repo 无 design/任务卡清单(无清单=不校验),无相交可判
|
|
1205
|
+
const face = [...repoSet];
|
|
886
1206
|
for (const f of files) {
|
|
887
|
-
if (!
|
|
888
|
-
|
|
889
|
-
if (repoKey === 'main') reviewAdmittedFiles.push(f);
|
|
1207
|
+
if (!face.some(ap => pathMatches(f, ap))) {
|
|
1208
|
+
if (repoKey === 'main') reviewForeignMain.push(f);
|
|
890
1209
|
}
|
|
891
1210
|
}
|
|
892
1211
|
}
|
|
893
|
-
if (
|
|
894
|
-
result.
|
|
1212
|
+
if (reviewForeignMain.length > 0) {
|
|
1213
|
+
result.reviewOverdeclaredFiles = reviewForeignMain;
|
|
895
1214
|
result.warnings = (result.warnings || []).concat([
|
|
896
|
-
`${
|
|
1215
|
+
`${reviewForeignMain.length} 个 review 声明文件不在 allow 面(design 清单/任务卡 allowed_paths/linked 声明),已剔除出放行面(review 声明了越权文件——嫌疑:review 失实或并行会话在途文件混入声明):${reviewForeignMain.join(', ')};确属本变更交付请补进 design §6 清单或任务卡 allowed_paths`
|
|
897
1216
|
]);
|
|
898
1217
|
}
|
|
899
1218
|
const allowSet = allowMap.get('main') || new Set();
|
|
@@ -933,16 +1252,90 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
|
|
|
933
1252
|
if (hasAllowList) {
|
|
934
1253
|
const violations = classifyAllowListViolations(changedFiles, allowSet);
|
|
935
1254
|
if (violations.length > 0) {
|
|
1255
|
+
// D-003:violation 分流——已被 review 声明但不在 allow 面(越权声明)与完全未声明分报,
|
|
1256
|
+
// 前者带「review 声明了越权文件」嫌疑标注(外来声明剔除后的落地拦截报告行)。
|
|
1257
|
+
const foreignSet = new Set(result.reviewOverdeclaredFiles || []);
|
|
1258
|
+
const foreignViolations = violations.filter(f => foreignSet.has(f));
|
|
1259
|
+
const undeclaredViolations = violations.filter(f => !foreignSet.has(f));
|
|
936
1260
|
result.extraFiles.push(...violations);
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
1261
|
+
const errLines = [];
|
|
1262
|
+
if (undeclaredViolations.length > 0) {
|
|
1263
|
+
errLines.push(
|
|
1264
|
+
`文件清单校验失败:以下变更文件不在 design.md 清单、也不在 task review.json changedFiles 声明中:\n ${undeclaredViolations.join('\n ')}\n(若属执行期合理偏差:在 design.md §6 清单或任务卡 allowed_paths 补行——review.json 的 changedFiles 声明已不再放行清单外文件,仅作审计对照)`
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
if (foreignViolations.length > 0) {
|
|
1268
|
+
errLines.push(
|
|
1269
|
+
`review 声明了越权文件:以下变更文件已被 task review.json changedFiles 声明,但不在 allow 面(design 清单/任务卡 allowed_paths/linked 声明)——嫌疑:review 失实或并行会话在途文件混入声明,不予放行:\n ${foreignViolations.join('\n ')}\n(确属本变更交付:补进 design.md §6 清单或任务卡 allowed_paths 后重跑)`
|
|
1270
|
+
);
|
|
1271
|
+
}
|
|
1272
|
+
result.errors.push(errLines.join('\n'));
|
|
940
1273
|
// checkOnly(assess)模式不短路:继续跑 Gate3,收集所有道供一次报全(坑 worktree-execute-apply-friction 坑4)。
|
|
941
1274
|
// 真实 apply(checkOnly=false)仍短路,保安全。
|
|
942
1275
|
if (!checkOnly) return result;
|
|
943
1276
|
}
|
|
944
1277
|
}
|
|
945
1278
|
|
|
1279
|
+
// --- 4.3 guard 相交预检(task-02 / FR-03 / D-002@v1):活跃 quick 会话 guard.allowedFiles ∩ 本次落盘集 ---
|
|
1280
|
+
// 内嵌 applyWorktree 主流程:CLI apply(index.js apply 分支)与 assess 自动 apply(index.js assess
|
|
1281
|
+
// 分支)全经此覆盖;真实 apply 由调用方 withMainRepoLock 包裹——判定与落盘同在锁内,无 TOCTOU。
|
|
1282
|
+
// 位置在一切主仓写动作之前(4.4 stash / 4.5 合并写回 / 4.6 --merge / step7 patch / ENOBUFS 降级)。
|
|
1283
|
+
// applyFiles=changedFiles∪newPatchFiles:newPatchFiles⊆patchFiles⊆changedFiles(resolvePatchFiles
|
|
1284
|
+
// 是 changedFiles 的过滤),并集即 changedFiles∪patchFiles,此处显式两集合并忠于 result 实际字段口径。
|
|
1285
|
+
// specBase/sessionsDir 复用上方 allowlist 同一解析链(allowSpecBase > 平台指针 > 本地),勿新造配置读取。
|
|
1286
|
+
// checkOnly 只读零写盘 → 跳过(零行为变化)。无活跃会话/无声明/交集空 → 全段空转零路径变化。
|
|
1287
|
+
if (!checkOnly) {
|
|
1288
|
+
const guardSpecBase = allowSpecBase || _silentPointerSpecRoot(projectRoot) || join(projectRoot, '.sillyspec');
|
|
1289
|
+
const guardSessionsDir = allowRuntimeRoot ? join(allowRuntimeRoot, 'quick-sessions') : undefined;
|
|
1290
|
+
const activeGuards = collectActiveQuickGuardFiles(guardSpecBase, { excludeChange: changeName, sessionsDir: guardSessionsDir });
|
|
1291
|
+
const guardApplyFiles = [...new Set([...changedFiles, ...resolvePatchFiles(changedFiles, allowSet, hasAllowList)])];
|
|
1292
|
+
const guardCheck = checkGuardOverlap({ activeGuards, applyFiles: guardApplyFiles, selfChange: changeName });
|
|
1293
|
+
if (guardCheck.blocked) {
|
|
1294
|
+
const sessions = [...new Set(guardCheck.overlaps.map(o => o.sessionId))];
|
|
1295
|
+
const files = [...new Set(guardCheck.overlaps.map(o => o.file))];
|
|
1296
|
+
const pairLines = guardCheck.overlaps.map(o => ` [${o.sessionId}] ${o.file}`);
|
|
1297
|
+
if (force) {
|
|
1298
|
+
// 显式解锁:放行并留痕(审计可见——自动路径永不带 force,overlapForced 只可能来自人工 --force)
|
|
1299
|
+
result.overlapForced = { sessions, files };
|
|
1300
|
+
result.warnings = (result.warnings || []).concat([
|
|
1301
|
+
`--force 越过 guard 相交预检:${guardCheck.overlaps.length} 个会话×文件对与活跃 quick 会话声明重叠——` +
|
|
1302
|
+
pairLines.join(';') + `(对方会话在途改动可能被覆盖,overlapForced 留痕)`
|
|
1303
|
+
]);
|
|
1304
|
+
} else if (autoApply) {
|
|
1305
|
+
// 软跳过:无人值守不越权(不落地互冲)也不阻断审计流——不抛错,指引人工评估
|
|
1306
|
+
result.overlapSkipped = true;
|
|
1307
|
+
result.warnings = (result.warnings || []).concat([
|
|
1308
|
+
`自动 apply 软跳过(未落盘):${files.length} 个文件与活跃 quick 会话在途声明重叠——` +
|
|
1309
|
+
pairLines.join(';') + `——请人工评估后显式 sillyspec worktree apply ${changeName}(确认无冲突),或等对方会话 --done 后重试`
|
|
1310
|
+
]);
|
|
1311
|
+
return result; // 安全返回:result.ok 保持 false,主仓零写动作(本预检位于一切写动作之前)
|
|
1312
|
+
} else {
|
|
1313
|
+
// 人工 CLI 入口(fail-closed):结构化错误含会话×文件对清单 + 串行化指引 + --force 提示
|
|
1314
|
+
result.errors.push(
|
|
1315
|
+
`guard 相交拦截:本次 apply 文件与 ${sessions.length} 个活跃 quick 会话的在途声明重叠(会话×文件对):\n` +
|
|
1316
|
+
pairLines.join('\n') +
|
|
1317
|
+
`\n串行化指引:等对方会话 sillyspec run quick --done 完成(guard 目录随之清理)后重试;` +
|
|
1318
|
+
`或确认无实际冲突后显式解锁:sillyspec worktree apply ${changeName} --force(overlapForced 留痕)`
|
|
1319
|
+
);
|
|
1320
|
+
return result; // index.js apply 分支对 result.errors 走 exit 1 展示
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
// ── ql-20260915-001 修复①:mismatch 前置合并的 dirty 排除集(4.4 stash 前取样)──
|
|
1326
|
+
// 4.5b 的 EXCLUDE-MISMATCH 前置合并只碰「主仓无未提交改动」的 mismatch 文件——dirty∩mismatch
|
|
1327
|
+
// 交 4.5 脏读合并处理(其 ours=主仓磁盘脏内容,已含已提交推进,语义更全)。排除集必须在
|
|
1328
|
+
// 4.4 --stash-dirty 之前取样:stash 会把脏文件收走,事后取 diff 恒空,会把在途文件误判成
|
|
1329
|
+
// 干净 mismatch 用 ours=HEAD 合并写回(stash 恢复时与合并写回互踩)。checkOnly 只读不合并,
|
|
1330
|
+
// 无 mismatch 时零开销,均不取样。untracked 不可能是 mismatch(不在 HEAD,mainBlob=null
|
|
1331
|
+
// 天然被合并函数的 ours=git show HEAD:f 失败挡住),此处只取 tracked-modified 口径即可。
|
|
1332
|
+
let preStashDirtySet = null
|
|
1333
|
+
if (!checkOnly && (result.hashMismatchFiles || []).length > 0) {
|
|
1334
|
+
preStashDirtySet = new Set(
|
|
1335
|
+
(gitQuiet(projectRoot, ['diff', '--name-only', 'HEAD']) || '').split('\n').filter(Boolean)
|
|
1336
|
+
)
|
|
1337
|
+
}
|
|
1338
|
+
|
|
946
1339
|
// --- 4.4 --stash-dirty:主仓在途改动自动 stash(坑 apply-main-dirty-no-first-class, ---
|
|
947
1340
|
// 2026-08-24 用户反馈四期①:主仓并行在途改动下默认 / --skip-overlap(全重叠「无可应用子集」)/
|
|
948
1341
|
// --merge(git 拒在脏树启动合并)三路死锁,手工 stash→3way→pop 流程未内置。flag 显式 opt-in
|
|
@@ -984,7 +1377,45 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
|
|
|
984
1377
|
// 用 --merge 时跳过未提交 dirty 拦截——merge 同样要求工作区相对干净,此处仅提示风险,真正失败由 applyByMerge 报告。
|
|
985
1378
|
// keepConflicts:true——显式 --merge 冲突时保留现场供手工解决(不再直接 abort 丢上下文)。
|
|
986
1379
|
if (merge && !checkOnly) {
|
|
987
|
-
return applyByMerge(result, changeName, projectRoot, wm, { keepConflicts: true });
|
|
1380
|
+
return applyByMerge(result, changeName, projectRoot, wm, { keepConflicts: true, specBase: allowSpecBase });
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
// --- 4.5b EXCLUDE-MISMATCH 前置三方合并(ql-20260915-001 修复①,坑 apply-archived-mismatch-no-merge,
|
|
1384
|
+
// 2026-09-14 用户实证:归档后补 apply,主仓已被并行会话提交推进 → rescue EXCLUDE-MISMATCH 跳过
|
|
1385
|
+
// 不落地,被迫手工 cp+锚点合并)---
|
|
1386
|
+
// 对 hashMismatch ∩ changedFiles 且主仓无未提交改动(pre-stash dirty 集排除,dirty 面交 4.5
|
|
1387
|
+
// 脏读合并)的文件逐个 merge-file:base=checkpoint(deliverableBase)blob、ours=主仓 HEAD blob、
|
|
1388
|
+
// theirs=worktree 文件。clean → 写回主仓 + 批末 add + mergedMismatchFiles 留痕 + 从
|
|
1389
|
+
// changedFiles/hashMismatchFiles/deletedFiles 剔除(后续 patch 不再重放——staged 合并内容若再进
|
|
1390
|
+
// patch,apply --3way 对 staged≠HEAD 的文件报 does not match index);conflict/异常 → 留原集走
|
|
1391
|
+
// 既有出口(rescue EXCLUDE-MISMATCH 新文案 / step7 --3way 实测 / --merge 兜底)。
|
|
1392
|
+
// checkOnly(assess 只读)零写盘不试合并;--merge 已在 4.6 早退不达此;位于 4.3 guard 预检
|
|
1393
|
+
// (一切主仓写动作之前)与 Gate1 清单校验之后,越权文件不会被合并落地。
|
|
1394
|
+
if (!checkOnly && (result.hashMismatchFiles || []).length > 0 && worktreePath && worktreePath !== projectRoot) {
|
|
1395
|
+
const mismatchCandidates = result.hashMismatchFiles.filter(f =>
|
|
1396
|
+
changedFiles.includes(f) && !(preStashDirtySet && preStashDirtySet.has(f)))
|
|
1397
|
+
if (mismatchCandidates.length > 0) {
|
|
1398
|
+
try {
|
|
1399
|
+
const m3m = mergeMismatchThreeWay({ projectRoot, worktreePath, baseHash: deliverableBase }, mismatchCandidates)
|
|
1400
|
+
if (m3m.merged.length > 0) {
|
|
1401
|
+
result.mergedMismatchFiles = (result.mergedMismatchFiles || []).concat(m3m.merged)
|
|
1402
|
+
if (m3m.stagedOk === false) {
|
|
1403
|
+
result.warnings = (result.warnings || []).concat([
|
|
1404
|
+
`EXCLUDE-MISMATCH 三方合并 ${m3m.merged.length} 个文件已写回主仓,但批末 git add 暂存失败(未暂存对 git restore / git clean 不免疫)——请尽快人工 git add -- <该批文件> 锁定`
|
|
1405
|
+
])
|
|
1406
|
+
}
|
|
1407
|
+
result.warnings = (result.warnings || []).concat([
|
|
1408
|
+
`EXCLUDE-MISMATCH 前置三方合并:${m3m.merged.length} 个主干已推进文件 clean 合并已写回并暂存(主仓已提交推进与 worktree 交付均保留,覆写前原文备份于 .sillyspec/.runtime/merge-backups/)`
|
|
1409
|
+
])
|
|
1410
|
+
const mergedMisSet = new Set(m3m.merged)
|
|
1411
|
+
changedFiles = changedFiles.filter(f => !mergedMisSet.has(f))
|
|
1412
|
+
result.changedFiles = changedFiles
|
|
1413
|
+
result.deletedFiles = deletedFiles.filter(f => !mergedMisSet.has(f))
|
|
1414
|
+
result.hashMismatchFiles = result.hashMismatchFiles.filter(f => !mergedMisSet.has(f))
|
|
1415
|
+
}
|
|
1416
|
+
// m3m.conflicts 面留在 result.hashMismatchFiles 原样(rescue 分类 / step7 --3way 既有路径)
|
|
1417
|
+
} catch { /* 合并链路异常 → mismatch 集原样,走既有出口(fail-soft 不阻断 apply) */ }
|
|
1418
|
+
}
|
|
988
1419
|
}
|
|
989
1420
|
|
|
990
1421
|
// --- 4.5 校验:主工作区「未提交」脏文件是否与本次变更重叠(overlap-only 拦截)---
|
|
@@ -1013,10 +1444,14 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
|
|
|
1013
1444
|
// hash 必变,若仍比对会永久死锁(须手改 meta.baselineHash)。改判「排除规则下当前是否有未提交 dirty」。
|
|
1014
1445
|
const hasUncommittedDirty = staged !== '' || unstaged !== '' || untracked !== '';
|
|
1015
1446
|
if (hasUncommittedDirty) {
|
|
1447
|
+
// ql-20260915-001 修复①:4.5b 已合并写回并暂存的 mismatch 文件不出现在此脏清单——它们
|
|
1448
|
+
// 是本次 apply 自己的产物(非他者无关脏文件),列进「无关未提交文件」警告会误导。
|
|
1449
|
+
// 对 overlapDirty 无影响(合并文件已从 changedFiles 剔除,交集恒空);集合空时为恒等过滤。
|
|
1450
|
+
const mergedMisSet45 = new Set(result.mergedMismatchFiles || [])
|
|
1016
1451
|
const dirtyFiles = [...new Set(
|
|
1017
1452
|
((gitQuiet(projectRoot, ['diff', '--name-only', 'HEAD']) || '').split('\n').filter(Boolean))
|
|
1018
1453
|
.concat((gitQuiet(projectRoot, ['ls-files', '--others', '--exclude-standard']) || '').split('\n').filter(Boolean))
|
|
1019
|
-
)].filter(f => !f.startsWith('.sillyspec/') && f !== 'meta.json');
|
|
1454
|
+
)].filter(f => !f.startsWith('.sillyspec/') && f !== 'meta.json' && !mergedMisSet45.has(f));
|
|
1020
1455
|
const overlapDirty = dirtyFiles.filter(f => changedFiles.includes(f));
|
|
1021
1456
|
if (overlapDirty.length > 0) {
|
|
1022
1457
|
// 自动三方合并(坑 apply-dirty-block-no-merge,2026-09-10 驾驭小结第六批③):先对
|
|
@@ -1029,6 +1464,13 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
|
|
|
1029
1464
|
const m3 = mergeDirtyOverlapThreeWay({ projectRoot, worktreePath, baseHash: deliverableBase }, overlapDirty);
|
|
1030
1465
|
if (m3.merged.length > 0) {
|
|
1031
1466
|
result.mergedDirtyFiles = (result.mergedDirtyFiles || []).concat(m3.merged);
|
|
1467
|
+
if (m3.stagedOk === false) {
|
|
1468
|
+
// 批末 git add fail-open 留痕(task-01 / FR-01):写回成功但未暂存——result 面
|
|
1469
|
+
// 可见(console.warn 已在 mergeDirtyOverlapThreeWay 内打),提示人工补 add。
|
|
1470
|
+
result.warnings = (result.warnings || []).concat([
|
|
1471
|
+
`EXCLUDE-DIRTY 三方合并 ${m3.merged.length} 个文件已写回主仓,但批末 git add 暂存失败(未暂存对 git restore / git clean 不免疫)——请尽快人工 git add -- <该批文件> 锁定`
|
|
1472
|
+
]);
|
|
1473
|
+
}
|
|
1032
1474
|
const mergedSet = new Set(m3.merged);
|
|
1033
1475
|
changedFiles = changedFiles.filter(f => !mergedSet.has(f));
|
|
1034
1476
|
result.changedFiles = changedFiles;
|
|
@@ -1092,11 +1534,15 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
|
|
|
1092
1534
|
} else {
|
|
1093
1535
|
// 无关脏文件放行(只校验重叠文件):--3way 若因 autocrlf 报 does not match index 会被
|
|
1094
1536
|
// step7 catch 回滚(交集空前提下无损,见上注释),stash 后重试即可——不再硬挡 rescue 手动路径
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1537
|
+
// (ql-20260915-001 修复①:dirtyFiles 已剔除本次合并写回面,剔除后为空 = 探针命中的
|
|
1538
|
+
// 全是本 apply 自身 staged 产物——不打「0 个无关文件」的空警告)
|
|
1539
|
+
if (dirtyFiles.length > 0) {
|
|
1540
|
+
result.warnings = (result.warnings || []).concat([
|
|
1541
|
+
`主工作区有 ${dirtyFiles.length} 个与本次 apply 无关的未提交文件,已放行(只校验重叠文件):` +
|
|
1542
|
+
`${dirtyFiles.slice(0, 5).join(', ')}${dirtyFiles.length > 5 ? ' 等' : ''}` +
|
|
1543
|
+
`——若 apply --3way 因 CRLF/autocrlf 报 does not match index,可 git stash 后重试`
|
|
1544
|
+
])
|
|
1545
|
+
}
|
|
1100
1546
|
}
|
|
1101
1547
|
}
|
|
1102
1548
|
}
|
|
@@ -1220,6 +1666,26 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
|
|
|
1220
1666
|
if (patchContent.length === 0) {
|
|
1221
1667
|
// patch 为空(清单中部分文件可能没实际变更)
|
|
1222
1668
|
result.ok = true;
|
|
1669
|
+
// apply-manifest(task-01 出口③变体):4.5 三方合并全部消解重叠时,merge 写回面是
|
|
1670
|
+
// 本出口唯一落盘面——manifest 仍须覆盖(写回批末 add 已先行,指纹取 staged 态)。
|
|
1671
|
+
// ql-20260915-001 修复①:4.5b mismatch 前置合并同样可能消解全部 changedFiles(本出口
|
|
1672
|
+
// 另一来源),合并面并入 manifest files。
|
|
1673
|
+
const emptyPatchManifestFace = [...(result.mergedDirtyFiles || []), ...(result.mergedMismatchFiles || [])]
|
|
1674
|
+
if (emptyPatchManifestFace.length > 0) {
|
|
1675
|
+
try {
|
|
1676
|
+
const mf = writeApplyManifest({
|
|
1677
|
+
projectRoot, specBase: allowSpecBase, changeName,
|
|
1678
|
+
baseHash: deliverableBase, files: emptyPatchManifestFace,
|
|
1679
|
+
});
|
|
1680
|
+
if (mf && mf.written) result.applyManifest = mf.file;
|
|
1681
|
+
} catch (e) {
|
|
1682
|
+
result.warnings = (result.warnings || []).concat([
|
|
1683
|
+
`apply-manifest.json 写入失败(不影响 apply 结果;doctor 漂移检测将按无 manifest 跳过): ${(e.message || '').split('\n')[0]}`
|
|
1684
|
+
]);
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
// 坑 mixed-baseline-drift-hint(ql-20260915-004):三方合并消解型成功出口的混合基线提示
|
|
1688
|
+
annotateCommittedDrift(result, { projectRoot, baseHash: deliverableBase, files: emptyPatchManifestFace });
|
|
1223
1689
|
rmSync(tmpDir, { recursive: true, force: true });
|
|
1224
1690
|
return result;
|
|
1225
1691
|
}
|
|
@@ -1232,10 +1698,9 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
|
|
|
1232
1698
|
// 回滚准备:记录 patch 涉及的 tracked 文件(--3way 冲突后需恢复 HEAD 版,不留半成品冲突标记)。
|
|
1233
1699
|
// tracked 文件冲突 → git checkout -- <f> 还原;untracked 新建文件(--3way 不会对其冲突)不在回滚范围,
|
|
1234
1700
|
// 但若 --3way 部分成功后冲突,已创建的新文件需删,故记录 untracked 集合。
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
});
|
|
1701
|
+
// 批 D-③:per-file cat-file N+1 spawn 改单次 ls-tree blob 哈希表(同 :1188 既有口径复用)
|
|
1702
|
+
const headBlobMap = getBlobHashMap(projectRoot, 'HEAD', patchFiles)
|
|
1703
|
+
const trackedPatchFiles = patchFiles.filter(f => headBlobMap.has(f));
|
|
1239
1704
|
const newPatchFiles = patchFiles.filter(f => !trackedPatchFiles.includes(f));
|
|
1240
1705
|
|
|
1241
1706
|
// apply --3way 正式应用(主干已提交推进时自动三路合并)
|
|
@@ -1271,12 +1736,43 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
|
|
|
1271
1736
|
|
|
1272
1737
|
result.ok = true;
|
|
1273
1738
|
|
|
1739
|
+
// --- 7.2 apply 成功尾声写 apply-manifest.json(task-01 / FR-02 / D-001@v1 后半)---
|
|
1740
|
+
// 三条成功出口之一(patch 主路径;含 4.5 三方合并写回后主流程继续至此的出口③)。指纹
|
|
1741
|
+
// 口径 = staged blob(git show :<path> 的 LF 规范态——apply --3way 隐含 --index 已暂存,
|
|
1742
|
+
// merge 写回批末 add 亦已先行)→ 与 doctor 漂移检测的 staged 态比对天然同基。files 面 =
|
|
1743
|
+
// patch 实际落盘集 ∪ 4.5 三方合并写回集(mergeDirtyFiles 已从 changedFiles 剔除,须并回);
|
|
1744
|
+
// apply 删除的文件 staged/磁盘皆无,函数内自然跳过。fail-open:写失败不阻断 apply
|
|
1745
|
+
// (warning 留痕)。已存在则覆盖(重放 apply 以最新为准)。锁内(调用方 withMainRepoLock)。
|
|
1746
|
+
// manifestFace 提升到 try 外(坑 mixed-baseline-drift-hint ql-20260915-004:drift 提示
|
|
1747
|
+
// 在 manifest 写点后消费同面,try 内 const 出块即失域)
|
|
1748
|
+
const manifestFace = [...new Set([...patchFiles, ...(result.mergedDirtyFiles || []), ...(result.mergedMismatchFiles || [])])];
|
|
1749
|
+
try {
|
|
1750
|
+
// ql-20260915-001 修复①:mergedMismatchFiles(4.5b 前置合并写回面)与 mergedDirtyFiles
|
|
1751
|
+
// 同为 staged 落盘集,一并进 manifest 指纹面与提交复用 pathspec(缺列 = doctor 漂移检测
|
|
1752
|
+
// 盲区 + agent 按 pathspec 提交漏文件)
|
|
1753
|
+
const mf = writeApplyManifest({
|
|
1754
|
+
projectRoot, specBase: allowSpecBase, changeName,
|
|
1755
|
+
baseHash: deliverableBase, files: manifestFace,
|
|
1756
|
+
});
|
|
1757
|
+
if (mf && mf.written) result.applyManifest = mf.file;
|
|
1758
|
+
} catch (e) {
|
|
1759
|
+
result.warnings = (result.warnings || []).concat([
|
|
1760
|
+
`apply-manifest.json 写入失败(不影响 apply 结果;doctor 漂移检测将按无 manifest 跳过): ${(e.message || '').split('\n')[0]}`
|
|
1761
|
+
]);
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
// 坑 mixed-baseline-drift-hint(ql-20260915-004):patch 主路径成功出口的混合基线提示
|
|
1765
|
+
//(自身交付 staged 未提交 → baseHash..HEAD 即纯他者推进面,精确/变体命中均为真信号)
|
|
1766
|
+
annotateCommittedDrift(result, { projectRoot, baseHash: deliverableBase, files: manifestFace });
|
|
1767
|
+
|
|
1274
1768
|
// --- 7.5 提交复用 pathspec(坑 apply-commit-pathspec-sweep,2026-08-21 实证)---
|
|
1275
1769
|
// apply 后主仓常混有无关未提交文件(他者会话/并行 quick),agent 习惯 `git add <目录>/`
|
|
1276
1770
|
// 会把无关文件扫进暂存需手工剔除。落盘本变更精确 pathspec 供提交直接复用:
|
|
1277
1771
|
// 文件 + result 字段(CLI 输出可照抄 git add -- … / 长清单用 --pathspec-from-file)。
|
|
1278
1772
|
try {
|
|
1279
|
-
|
|
1773
|
+
// ql-20260915-001 修复①:提交复用 pathspec 补并两个三方合并写回面(均为 staged 落盘、
|
|
1774
|
+
// 属本变更交付——缺列时 agent 按此 pathspec 提交会漏掉合并成果)
|
|
1775
|
+
const commitFiles = [...new Set([...patchFiles, ...(result.mergedDirtyFiles || []), ...(result.mergedMismatchFiles || [])])].sort().filter(Boolean);
|
|
1280
1776
|
result.commitPathspec = commitFiles;
|
|
1281
1777
|
const runtimeRoot = join(projectRoot, '.sillyspec', '.runtime');
|
|
1282
1778
|
mkdirSync(runtimeRoot, { recursive: true });
|
|
@@ -1323,7 +1819,7 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
|
|
|
1323
1819
|
);
|
|
1324
1820
|
try {
|
|
1325
1821
|
// keepConflicts:false——自动降级路径无人善后冲突状态,维持 abort 回滚干净态
|
|
1326
|
-
return applyByMerge(result, changeName, projectRoot, wm, { keepConflicts: false });
|
|
1822
|
+
return applyByMerge(result, changeName, projectRoot, wm, { keepConflicts: false, specBase: allowSpecBase });
|
|
1327
1823
|
} catch (mergeErr) {
|
|
1328
1824
|
result.errors.push(`ENOBUFS 降级 merge 也失败: ${mergeErr.message}`);
|
|
1329
1825
|
return result;
|
|
@@ -1677,8 +2173,11 @@ function preAlignBaselineToMain({ meta, branch, projectRoot, result }) {
|
|
|
1677
2173
|
// git add + git commit;或 git merge --abort 放弃)。worktree/分支/meta 本就保留。
|
|
1678
2174
|
// false(ENOBUFS 自动降级路径,默认)→ 维持 abort——自动降级无人善后冲突状态,回滚到
|
|
1679
2175
|
// 干净态让用户显式重来更安全。
|
|
2176
|
+
// opts.specBase(task-01,2026-09-14-apply-conflict-hardening):applyWorktree 调用方透传的
|
|
2177
|
+
// spec 基准目录(平台模式 ctx.platformOpts.specRoot),成功尾声 writeApplyManifest 用;缺省
|
|
2178
|
+
// 时函数内同链回退(指针 specRoot > 本地 .sillyspec)——外部直接调用零必填。
|
|
1680
2179
|
export function applyByMerge(result, changeName, projectRoot, wm, opts = {}) {
|
|
1681
|
-
const { keepConflicts = false } = opts;
|
|
2180
|
+
const { keepConflicts = false, specBase } = opts;
|
|
1682
2181
|
const meta = wm.getMeta(changeName);
|
|
1683
2182
|
const changedFiles = result.changedFiles || [];
|
|
1684
2183
|
// 用 meta.branch(native-worktree 模式分支名可能不是 sillyspec/<change>),不硬编码。
|
|
@@ -1691,6 +2190,10 @@ export function applyByMerge(result, changeName, projectRoot, wm, opts = {}) {
|
|
|
1691
2190
|
// 它跳过 dirty 文件(不覆盖 WIP)且只提交对齐集;WIP commit 在后,两者不混提交 ---
|
|
1692
2191
|
autoCommitWorktreeWip({ meta, result, changeName });
|
|
1693
2192
|
|
|
2193
|
+
// 坑 mixed-baseline-drift-hint(ql-20260915-004):merge 前 HEAD = 纯他者推进面锚点——
|
|
2194
|
+
// merge 提交后 HEAD 含自身交付,drift 检测须以 preMergeHead 为推进面终点(防自污染误报)
|
|
2195
|
+
const preMergeHead = gitQuiet(projectRoot, ['rev-parse', 'HEAD']);
|
|
2196
|
+
|
|
1694
2197
|
try {
|
|
1695
2198
|
git(projectRoot, ['merge', '--no-ff', branch], { timeout: 30000 });
|
|
1696
2199
|
} catch (e) {
|
|
@@ -1755,6 +2258,32 @@ export function applyByMerge(result, changeName, projectRoot, wm, opts = {}) {
|
|
|
1755
2258
|
}
|
|
1756
2259
|
|
|
1757
2260
|
result.ok = true;
|
|
2261
|
+
// apply 成功尾声写 apply-manifest.json(task-01 出口②:applyWorktree 显式 --merge 与
|
|
2262
|
+
// ENOBUFS 自动降级两条提前 return 入口共用本成功出口)。merge 已 commit → staged 态 =
|
|
2263
|
+
// HEAD = 合并内容,指纹口径(git show :<path> LF 规范态)与 patch 路径同基;files 面 =
|
|
2264
|
+
// changedFiles(落地校验过的交付集)∪ mergedDirtyFiles(ENOBUFS 路径下 4.5 可能已写回)
|
|
2265
|
+
// ∪ mergedMismatchFiles(ql-20260915-001 修复①:4.5b 前置合并写回面,同口径并入),
|
|
2266
|
+
// merge 删除面 staged/磁盘皆无、函数内自然跳过。fail-open:写失败不阻断 merge(warning 留痕)。
|
|
2267
|
+
// manifestFace 提升到 try 外(坑 mixed-baseline-drift-hint ql-20260915-004:drift 提示在
|
|
2268
|
+
// manifest 写点后消费同面,try 内 const 出块即失域)
|
|
2269
|
+
const manifestFace = [...new Set([...(result.changedFiles || []), ...(result.mergedDirtyFiles || []), ...(result.mergedMismatchFiles || [])])];
|
|
2270
|
+
try {
|
|
2271
|
+
const mf = writeApplyManifest({
|
|
2272
|
+
projectRoot, specBase, changeName,
|
|
2273
|
+
baseHash: meta.baselineCommit || meta.baseHash, files: manifestFace,
|
|
2274
|
+
});
|
|
2275
|
+
if (mf && mf.written) result.applyManifest = mf.file;
|
|
2276
|
+
} catch (e) {
|
|
2277
|
+
result.warnings = (result.warnings || []).concat([
|
|
2278
|
+
`apply-manifest.json 写入失败(不影响 merge 结果;doctor 漂移检测将按无 manifest 跳过): ${(e.message || '').split('\n')[0]}`
|
|
2279
|
+
]);
|
|
2280
|
+
}
|
|
2281
|
+
// 坑 mixed-baseline-drift-hint(ql-20260915-004):merge 成功出口的混合基线提示(推进面
|
|
2282
|
+
// 终点 = preMergeHead,剥离本 merge 自身交付,保精确命中为真信号)
|
|
2283
|
+
annotateCommittedDrift(result, {
|
|
2284
|
+
projectRoot, baseHash: meta.baselineCommit || meta.baseHash,
|
|
2285
|
+
files: manifestFace, head: preMergeHead || 'HEAD',
|
|
2286
|
+
});
|
|
1758
2287
|
try { result.mergeSummary = git(projectRoot, ['log', '--oneline', '-1']); } catch {}
|
|
1759
2288
|
try {
|
|
1760
2289
|
consumeCleanupResult(wm.cleanup(changeName, { force: true }), result);
|
|
@@ -1842,6 +2371,9 @@ export function assessApplyRisk(changeName, { cwd } = {}) {
|
|
|
1842
2371
|
);
|
|
1843
2372
|
// review.json 声明偏差文件(坑 apply-undeclared-deviation-block):与顺带修复同等待遇——
|
|
1844
2373
|
// reviewer 声明(已过 git 证据校验)的执行期偏差豁免 allowed_paths 严格校验,降 warning 注明来源。
|
|
2374
|
+
// D-003@v1(task-03)后本豁免仅作用于 Gate2 的 allowed_paths 归属道——allow 面(design∪task
|
|
2375
|
+
// 清单∪linked)之外的外来声明已在 Gate1(applyWorktree 3b 相交过滤)被剔除拦截,assess 整体
|
|
2376
|
+
// 判定以 Gate1 收集的 errors 为准(BLOCKED);两道口径不再重叠放行。
|
|
1845
2377
|
const reviewDeclaredSet = new Set(collectReviewDeclaredFiles(projectRoot, changeName, { runtimeRoot: join(assessSpecBase, '.runtime') }).get('main') || []);
|
|
1846
2378
|
|
|
1847
2379
|
// 检查 2: 变更在 allowed_paths 内(仅在 TaskCard 存在时);顺带修复/review 声明文件豁免。
|