sillyspec 3.28.9 → 3.28.10
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/full.json +1 -0
- package/package.json +1 -1
- package/src/change-list.js +73 -20
- package/src/config-schema.js +30 -0
- package/src/contract-matrix.js +84 -5
- package/src/cross-repo-reconcile.js +134 -0
- package/src/design-facts.js +56 -17
- package/src/index.js +18 -1
- package/src/knowledge-classify.js +20 -1
- package/src/probe7-anchor-check.js +69 -0
- package/src/quicklog.js +9 -4
- package/src/run/command.js +25 -2
- package/src/run/complete.js +70 -8
- package/src/run/gate-snapshot.js +110 -2
- package/src/run/gates.js +91 -7
- package/src/run/prompt.js +87 -6
- package/src/run/shared.js +71 -1
- package/src/scope-audit.js +101 -31
- package/src/stages/cmd-existence.js +9 -3
- package/src/stages/knowledge.js +24 -3
- package/src/stages/plan-postcheck.js +32 -5
- package/src/sync-noise.js +57 -1
- package/src/sync.js +8 -4
- package/src/task-review.js +75 -26
- package/src/verify-postcheck.js +192 -55
- package/src/verify-probes.js +390 -15
- package/src/worktree-apply.js +148 -5
- package/src/worktree-deps.js +24 -1
- package/src/worktree.js +274 -10
|
@@ -785,12 +785,19 @@ export function parseLocalYamlModules(yamlText) {
|
|
|
785
785
|
* modules 从 local.yaml 提取(monorepo 子包感知):无 modules 块时仅查根 package.json
|
|
786
786
|
* (与 scan-postcheck 历史行为一致);有 modules 块时多候选子包 package.json 任一命中即通过。
|
|
787
787
|
*
|
|
788
|
+
* 跨仓卡基准根切换(坑 verify-cmd-cross-repo-root,2026-09-15 wp 会话实证):卡片声明
|
|
789
|
+
* repo: <key> 时其 verify/implementation 里的路径相对**该仓**根——此前恒用主仓 projectRoot,
|
|
790
|
+
* 跨仓卡的 `cd src && npm run lint`(src 在跨仓里)解析到主仓 → 误报死命令。repoRegistry
|
|
791
|
+
* 命中该 key 时以跨仓根为该卡的 projectRoot(modules 是主仓 local.yaml 的相对子包路径,
|
|
792
|
+
* 跨仓卡不适用,置 null);repo 未注册时由检查 1 的注册校验报错,此处维持主仓根不重复报。
|
|
793
|
+
*
|
|
788
794
|
* @param {string} changeDir - 变更目录
|
|
789
795
|
* @param {string} projectRoot - 项目根目录(package.json 查找基准)
|
|
790
796
|
* @param {object|null} modules - local.yaml modules 块({ name: { path } }),可选
|
|
797
|
+
* @param {Map<string,string>|null} [repoRegistry=null] - local.yaml repos: 段(跨仓卡基准根解析)
|
|
791
798
|
* @returns {{ ok: boolean, errors: string[], warnings: string[] }}
|
|
792
799
|
*/
|
|
793
|
-
export function validateTaskCommands(changeDir, projectRoot, modules = null) {
|
|
800
|
+
export function validateTaskCommands(changeDir, projectRoot, modules = null, repoRegistry = null) {
|
|
794
801
|
const errors = []
|
|
795
802
|
const warnings = []
|
|
796
803
|
|
|
@@ -826,7 +833,16 @@ export function validateTaskCommands(changeDir, projectRoot, modules = null) {
|
|
|
826
833
|
const text = `${verifyText}\n${implText}`
|
|
827
834
|
if (!text.trim()) continue
|
|
828
835
|
|
|
829
|
-
|
|
836
|
+
// 跨仓卡基准根切换(坑 verify-cmd-cross-repo-root):repo: 命中注册表 → 该卡命令以跨仓
|
|
837
|
+
// 根解析;modules(主仓相对子包路径)对跨仓卡不适用置 null
|
|
838
|
+
const cardRepo = parseRepo(content)
|
|
839
|
+
let cardRoot = projectRoot
|
|
840
|
+
let cardModules = modules
|
|
841
|
+
if (cardRepo && cardRepo !== 'main' && repoRegistry && repoRegistry instanceof Map && repoRegistry.has(cardRepo)) {
|
|
842
|
+
cardRoot = repoRegistry.get(cardRepo)
|
|
843
|
+
cardModules = null
|
|
844
|
+
}
|
|
845
|
+
const { invalid } = validateScriptCommands(text, { projectRoot: cardRoot, modules: cardModules })
|
|
830
846
|
for (const inv of invalid) {
|
|
831
847
|
errors.push(`${taskId}: ${inv.cmd} 命令不存在(${inv.reason})`)
|
|
832
848
|
}
|
|
@@ -880,7 +896,7 @@ const FILE_LIST_SECTION_RE = /^#{2,3}\s*(?:\d+[.)]\s*)?(文件变更清单|变
|
|
|
880
896
|
* @param {string} designPath - design.md 绝对路径
|
|
881
897
|
* @returns {{ byRepo: Map<string, Set<string>>, hasSegmentHeader: boolean, allFiles: string[], malformedHeaders: string[] }}
|
|
882
898
|
*/
|
|
883
|
-
function parseDesignCoverageByRepo(designPath) {
|
|
899
|
+
export function parseDesignCoverageByRepo(designPath) {
|
|
884
900
|
const byRepo = new Map()
|
|
885
901
|
const allFiles = []
|
|
886
902
|
const malformedHeaders = []
|
|
@@ -1619,10 +1635,21 @@ export async function executePlanPostcheck(context) {
|
|
|
1619
1635
|
let taskModules = null
|
|
1620
1636
|
let repoRegistry = null
|
|
1621
1637
|
if (existsSync(localYamlPath)) {
|
|
1622
|
-
const localYamlText =
|
|
1638
|
+
const localYamlText = _readFileSync(localYamlPath, 'utf8')
|
|
1623
1639
|
taskModules = parseLocalYamlModules(localYamlText)
|
|
1624
1640
|
repoRegistry = parseRepoRegistry(localYamlText)
|
|
1625
1641
|
}
|
|
1642
|
+
// 坑 register-repo-specbase-split-brain(读侧对齐):平台模式下 specDir=平台 spec 根,此处
|
|
1643
|
+
// 读 spec 根 local.yaml 的 repos: 而 execute MultiRepoContext 恒读 <cwd>/.sillyspec/local.yaml
|
|
1644
|
+
// ——plan gate 看得到注册、execute 启动却 fail-closed(2026-09-15 wp 会话实证)。项目侧
|
|
1645
|
+
// local.yaml 存在时以它为 repos: 权威(register-repo 写侧已对齐项目侧);不存在(纯平台
|
|
1646
|
+
// 项目从未建过项目侧文件)维持 spec 根回退,零回归。
|
|
1647
|
+
const projectLocalYamlPath = pJoin(cwd, '.sillyspec', 'local.yaml')
|
|
1648
|
+
if (existsSync(projectLocalYamlPath)) {
|
|
1649
|
+
try {
|
|
1650
|
+
repoRegistry = parseRepoRegistry(_readFileSync(projectLocalYamlPath, 'utf8'))
|
|
1651
|
+
} catch { /* 项目侧读失败维持 spec 根结果 */ }
|
|
1652
|
+
}
|
|
1626
1653
|
|
|
1627
1654
|
// ── 1. 一致性校验 ──
|
|
1628
1655
|
const consistency = validateBlueprintConsistency(changeDir, { repoRegistry })
|
|
@@ -1656,7 +1683,7 @@ export async function executePlanPostcheck(context) {
|
|
|
1656
1683
|
// invalid → error 硬阻断,避免 execute 子代理跑死命令(design D-04 / 问题 3)。
|
|
1657
1684
|
// modules 块可选:无块时仅查根 package.json(与 scan-postcheck 历史行为一致)。
|
|
1658
1685
|
// local.yaml 已在检查 1 前预读(taskModules),此处直接消费。
|
|
1659
|
-
const taskCmds = validateTaskCommands(changeDir, context.cwd, taskModules)
|
|
1686
|
+
const taskCmds = validateTaskCommands(changeDir, context.cwd, taskModules, repoRegistry)
|
|
1660
1687
|
if (taskCmds.errors.length > 0) {
|
|
1661
1688
|
failures.push({
|
|
1662
1689
|
name: 'TaskCard 命令存在性校验(verify/implementation 的 npm/pnpm/yarn run <script> 不存在)',
|
package/src/sync-noise.js
CHANGED
|
@@ -243,6 +243,60 @@ export function noteChangeDeletedResolved(changeName) {
|
|
|
243
243
|
}
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
+
// ── push 409 自愈回执噪音闸(2026-09-15 复盘反馈:自愈重试不阻断但每步刷屏)──
|
|
247
|
+
// 与 change_deleted 闸同构,按 `${changeName}\u0000${kind}` 键控(kind=自竞态/内容一致/自回声
|
|
248
|
+
// 三类自愈)。语义边界:自愈是成功收敛路径不是故障——base_ts 409 每步自动同步都可能重撞、
|
|
249
|
+
// ts 逐次不同但零新信息;首报完整可见(保留原文 ⚠️ 口吻),10 分钟窗口内同变更同类型重复
|
|
250
|
+
// 自愈静默,窗口过期重报保活。推送成功不自愈清窗(下次 409 首报照常可见——竞态是否复发
|
|
251
|
+
// 本身是信号);SILLYSPEC_DEBUG_SYNC=1 与手动 platform sync(noMute)旁路。
|
|
252
|
+
const SELF_HEAL_MARKER = 'sync-noise-self-heal.json'
|
|
253
|
+
let _openedSelfHealByThisProcess = new Set()
|
|
254
|
+
const _seenSelfHealInProcess = new Set()
|
|
255
|
+
|
|
256
|
+
function _readSelfHealWindows() {
|
|
257
|
+
const p = _runtimeDir ? join(_runtimeDir, SELF_HEAL_MARKER) : null
|
|
258
|
+
if (!p) return null
|
|
259
|
+
try {
|
|
260
|
+
const s = JSON.parse(readFileSync(p, 'utf8'))
|
|
261
|
+
return s && typeof s === 'object' && !Array.isArray(s) ? s : null
|
|
262
|
+
} catch { return null }
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function _selfHealLineVisible(changeName, kind, noMute) {
|
|
266
|
+
if (!changeName || noMute || process.env.SILLYSPEC_DEBUG_SYNC) return true
|
|
267
|
+
const key = changeName + '\u0000' + kind
|
|
268
|
+
if (_seenSelfHealInProcess.has(key)) return false
|
|
269
|
+
const now = Date.now()
|
|
270
|
+
const windows = _readSelfHealWindows()
|
|
271
|
+
const until = windows ? windows[key] : undefined
|
|
272
|
+
if (typeof until === 'number' && now < until) {
|
|
273
|
+
_seenSelfHealInProcess.add(key)
|
|
274
|
+
// 开窗进程内同类自愈可见(诊断完整性),先前进程留的窗口静默
|
|
275
|
+
return _openedSelfHealByThisProcess.has(key)
|
|
276
|
+
}
|
|
277
|
+
_seenSelfHealInProcess.add(key)
|
|
278
|
+
const next = windows || {}
|
|
279
|
+
next[key] = now + MUTE_WINDOW_MS
|
|
280
|
+
try {
|
|
281
|
+
mkdirSync(_runtimeDir, { recursive: true })
|
|
282
|
+
writeFileSync(join(_runtimeDir, SELF_HEAL_MARKER), JSON.stringify(next) + '\n', 'utf8')
|
|
283
|
+
} catch { /* marker 写失败 = 闸门失效退化为直通,仅损失降噪 */ }
|
|
284
|
+
_openedSelfHealByThisProcess.add(key)
|
|
285
|
+
return true
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* push 409 自愈回执(warn 口吻:首报保留原文 ⚠️,窗口内同变更同类型静默)。
|
|
290
|
+
* @returns {boolean} true=已输出
|
|
291
|
+
*/
|
|
292
|
+
export function syncSelfHealWarn(changeName, kind, msg, { noMute = false } = {}) {
|
|
293
|
+
if (_selfHealLineVisible(changeName, kind, noMute)) {
|
|
294
|
+
console.warn(msg)
|
|
295
|
+
return true
|
|
296
|
+
}
|
|
297
|
+
return false
|
|
298
|
+
}
|
|
299
|
+
|
|
246
300
|
/**
|
|
247
301
|
* 任意一次同步成功:清 marker。此前处于闸内(marker 存在)时打一行恢复提示——
|
|
248
302
|
* 平台恢复的第一个信号应该可见,之后的成功照常静默(成功不打扰)。
|
|
@@ -256,11 +310,13 @@ export function noteSyncSuccess() {
|
|
|
256
310
|
console.log('[sync] 平台连接已恢复(此前静默的同步失败已停止)')
|
|
257
311
|
}
|
|
258
312
|
|
|
259
|
-
/** 测试复位:清进程内绑定与开窗状态(连接类闸 + change_deleted
|
|
313
|
+
/** 测试复位:清进程内绑定与开窗状态(连接类闸 + change_deleted 变更级闸 + 409 自愈闸)。 */
|
|
260
314
|
export function _resetSyncNoiseForTest() {
|
|
261
315
|
_runtimeDir = null
|
|
262
316
|
_openedByThisProcess = false
|
|
263
317
|
_seenInProcess.clear()
|
|
264
318
|
_openedDeletedByThisProcess = new Set()
|
|
265
319
|
_seenDeletedInProcess.clear()
|
|
320
|
+
_openedSelfHealByThisProcess = new Set()
|
|
321
|
+
_seenSelfHealInProcess.clear()
|
|
266
322
|
}
|
package/src/sync.js
CHANGED
|
@@ -18,7 +18,7 @@ import { safeGit } from './git-helper.js';
|
|
|
18
18
|
import { openDatabase } from './db-engine.js';
|
|
19
19
|
import { PLATFORM_MANAGED_FILENAME, QUICK_SID_RE } from './run/shared.js';
|
|
20
20
|
import { syncSpecTree } from './spec-sync.js';
|
|
21
|
-
import { bindSyncNoiseFromCwd, syncConnectionWarn, isConnectionClassStatus, noteSyncSuccess, syncChangeDeletedLog, syncChangeDeletedWarn, noteChangeDeletedResolved } from './sync-noise.js';
|
|
21
|
+
import { bindSyncNoiseFromCwd, syncConnectionWarn, isConnectionClassStatus, noteSyncSuccess, syncChangeDeletedLog, syncChangeDeletedWarn, noteChangeDeletedResolved, syncSelfHealWarn } from './sync-noise.js';
|
|
22
22
|
|
|
23
23
|
// sync 是 best-effort(网络失败只 warn):平台指针失效时不抛,跳过平台、回退本地。
|
|
24
24
|
function safePlatformSpecDir(cwd) {
|
|
@@ -844,7 +844,9 @@ export class SyncManager {
|
|
|
844
844
|
// 说明赢者是本机自己人——刷新 base_ts 重试一次即收敛,不落冲突文件(外来推送不可能
|
|
845
845
|
// 推进本机 DB 的 base_ts,不满足条件自然落回下方真冲突分支,零误放行)
|
|
846
846
|
if (attempt < MAX_PUSH_ATTEMPTS && await this._localBaseTsCovers(changeName, platformLastPushedAt)) {
|
|
847
|
-
|
|
847
|
+
// 409 自愈键控闸(2026-09-15 复盘:自愈不阻断但每步刷屏)——首报可见、窗口内同变更
|
|
848
|
+
// 同类型静默;手动 platform sync(manual)旁路
|
|
849
|
+
syncSelfHealWarn(changeName, 'self-race', `⚠️ [sync] push 409 自竞态判定:本机并发推送已回填 base_ts(平台 ts=${platformLastPushedAt}),刷新后自动重试`, { noMute: manual });
|
|
848
850
|
continue;
|
|
849
851
|
}
|
|
850
852
|
|
|
@@ -863,7 +865,8 @@ export class SyncManager {
|
|
|
863
865
|
'UPDATE changes SET last_synced_platform_ts = MAX(?, COALESCE(last_synced_platform_ts, ?)) WHERE name = ?'
|
|
864
866
|
).run(platformLastPushedAt || pushedAt, platformLastPushedAt || pushedAt, changeName);
|
|
865
867
|
} catch {}
|
|
866
|
-
|
|
868
|
+
// 409 自愈键控闸同上(kind=content-equal)
|
|
869
|
+
syncSelfHealWarn(changeName, 'content-equal', `⚠️ [sync] push 409 内容一致自愈: 平台 ts=${platformLastPushedAt} 为外来噪声重推(六表内容与本地一致),base_ts 已推进,不落冲突文件`, { noMute: manual });
|
|
867
870
|
return { synced: 1, errors: [], selfHealed: true, reason: 'push 409 平台内容与本地一致(外来噪声重推),base_ts 已推进' };
|
|
868
871
|
}
|
|
869
872
|
} catch { /* 比对失败维持原判(fail-closed 到真冲突分支) */ }
|
|
@@ -895,7 +898,8 @@ export class SyncManager {
|
|
|
895
898
|
pm._ensureDB(this.cwd).getDb().prepare(
|
|
896
899
|
'UPDATE changes SET last_synced_platform_ts = MAX(?, COALESCE(last_synced_platform_ts, ?)) WHERE name = ?'
|
|
897
900
|
).run(healTs, healTs, changeName);
|
|
898
|
-
|
|
901
|
+
// 409 自愈键控闸同上(kind=self-echo)
|
|
902
|
+
syncSelfHealWarn(changeName, 'self-echo', `⚠️ [sync] push 409 自回声判定:平台 ts=${platformLastPushedAt} 为本机自推回声(血统 ${platLin},推送者 ${platformLastPusher || '未回传·窗口判定'}),base_ts 已推进到 ${healTs},自动重推`, { noMute: manual });
|
|
899
903
|
if (attempt < MAX_PUSH_ATTEMPTS) continue;
|
|
900
904
|
// 重试额度耗尽(attempt=2 仍进此分支,理论少见):不落冲突文件——base 已推进,
|
|
901
905
|
// 下次常规同步按新 base 直接推送收敛
|
package/src/task-review.js
CHANGED
|
@@ -1249,6 +1249,70 @@ function resolveAttributionDiffFiles({ cwd, changeName, specBase, platformOpts =
|
|
|
1249
1249
|
return { mode: 'main-window', diffFiles: resolveVerifyChangedFiles(cwd, changeName, null, { specBase }), notes, meta, mergeGitDir }
|
|
1250
1250
|
}
|
|
1251
1251
|
|
|
1252
|
+
/**
|
|
1253
|
+
* worktree/in-place 改动文件集:porcelain 未提交 ∪ committed merge-base 补齐——口径单一化。
|
|
1254
|
+
* 口径出处:D-004@v1(worktree 归因唯一事实源 = worktree 分支 diff + porcelain,决策库
|
|
1255
|
+
* decisions/worktree.md)+ 变更 2026-09-15-worktree-dual-truth-gates §4 / FR-04;坑④
|
|
1256
|
+
* worktree-tick-guard-commit-only(2026-09-15 实证):勾选守卫 prefetchDiffFileSet
|
|
1257
|
+
* (run/complete.js)只算 base..head(已提交),子代理默认不 commit 时守卫恒空跳过勾选——
|
|
1258
|
+
* 本 helper 是「草稿归属并入段(generateTaskReviewDrafts)」与「勾选守卫(prefetchDiffFileSet)」
|
|
1259
|
+
* 两处的单一真相。已知残留 R-09:resolveVerifyChangedFiles(verify-postcheck.js)内另有同型
|
|
1260
|
+
* 补齐段(form A 专属:滤网保 .sillyspec/docs/** 交付物,与本 helper 的整前缀滤网不同),
|
|
1261
|
+
* 本次不动,待该函数下次触碰时统一。
|
|
1262
|
+
*
|
|
1263
|
+
* 行为契约:
|
|
1264
|
+
* - meta 缺省时内部 WorktreeManager.getMeta 自取(try/catch → null)。
|
|
1265
|
+
* - gitDir = meta.worktreePath(mode 非 in-place-fallback 且目录存在)否则 cwd——
|
|
1266
|
+
* in-place 退化取 cwd(保住现状 in-place 主仓 porcelain 并入,不丢,R-08);
|
|
1267
|
+
* meta 完全缺失时同样取 cwd(与被并入段 mergeGitDir 语义一致)。
|
|
1268
|
+
* - porcelain:status --porcelain --untracked-files=all(parsePorcelainFiles 解析),
|
|
1269
|
+
* 滤 .sillyspec 前缀(与被替换的草稿并入段同口径)。
|
|
1270
|
+
* - committed 补齐(仅 gitDir !== cwd,防双并):merge-base(主仓 HEAD, wtHEAD)..wtHEAD
|
|
1271
|
+
* 的 diff --name-only——worktree 内已 commit 的改动对主仓 base..head 与 status 双不可见
|
|
1272
|
+
* (「先提交则 diff 空」时序两难);git 失败 fail-open 忽略。
|
|
1273
|
+
* - 返回正斜杠归一去重数组;任何 git 异常 → 已收集部分或 [](fail-open,不阻断调用方)。
|
|
1274
|
+
*
|
|
1275
|
+
* @param {string} cwd 主仓根
|
|
1276
|
+
* @param {string} changeName 变更名
|
|
1277
|
+
* @param {object|null} [meta] 预取的 worktree meta(缺省内部 getMeta 自取)
|
|
1278
|
+
* @param {object} [opts] 预留(对齐 design 接口签名)
|
|
1279
|
+
* @returns {string[]} 正斜杠归一文件集(git 失败 → 已收集部分或 [],fail-open)
|
|
1280
|
+
*/
|
|
1281
|
+
export function collectWorktreeChangedFiles(cwd, changeName, meta = null, opts = {}) {
|
|
1282
|
+
const files = new Set()
|
|
1283
|
+
if (!cwd || !changeName) return []
|
|
1284
|
+
let m = meta
|
|
1285
|
+
if (m == null) {
|
|
1286
|
+
try { m = new WorktreeManager({ cwd }).getMeta(changeName) } catch { m = null }
|
|
1287
|
+
}
|
|
1288
|
+
const gitDir = (m?.worktreePath && m.mode !== 'in-place-fallback' && existsSync(m.worktreePath))
|
|
1289
|
+
? m.worktreePath
|
|
1290
|
+
: cwd
|
|
1291
|
+
const keep = (p) => p && p !== '.sillyspec' && !p.startsWith('.sillyspec/')
|
|
1292
|
+
// 未提交:worktree(in-place 退化=主仓)porcelain 全量(含 untracked)
|
|
1293
|
+
try {
|
|
1294
|
+
const st = runGit(gitDir, ['status', '--porcelain', '--untracked-files=all'], { trim: false })
|
|
1295
|
+
for (const p of parsePorcelainFiles(st)) if (keep(p)) files.add(p)
|
|
1296
|
+
} catch { /* porcelain 失败 → 返回已收集部分(fail-open) */ }
|
|
1297
|
+
// 已提交补齐:merge-base(主仓 HEAD, wtHEAD)(= 创建锚点)..wtHEAD 的 commit diff。
|
|
1298
|
+
// in-place(gitDir===cwd)时已提交改动在主仓 diff 内,跳过防双并;失败 fail-open 忽略。
|
|
1299
|
+
if (gitDir !== cwd) {
|
|
1300
|
+
try {
|
|
1301
|
+
const mainHead = runGit(cwd, ['rev-parse', 'HEAD'])
|
|
1302
|
+
const wtHead = runGit(gitDir, ['rev-parse', 'HEAD'])
|
|
1303
|
+
const mb = (mainHead && wtHead) ? runGit(gitDir, ['merge-base', wtHead, mainHead]) : null
|
|
1304
|
+
if (mb) {
|
|
1305
|
+
const out = String(runGit(gitDir, ['diff', '--name-only', mb, wtHead]) || '')
|
|
1306
|
+
for (let p of out.split('\n')) {
|
|
1307
|
+
p = p.replace(/^"|"$/g, '').replace(/\\/g, '/').trim()
|
|
1308
|
+
if (keep(p)) files.add(p)
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
} catch { /* 已提交补齐失败忽略(fail-open,不拖垮未提交并入) */ }
|
|
1312
|
+
}
|
|
1313
|
+
return [...files]
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1252
1316
|
/**
|
|
1253
1317
|
* worktree execute「主 agent 直接实现」模式收尾兜底:per-task review.json 缺失时,
|
|
1254
1318
|
* 据 git diff base..head 按 task allowed_paths 归属,自动落盘 cannot_verify 草稿。
|
|
@@ -1339,32 +1403,17 @@ export async function generateTaskReviewDrafts({ changeName, cwd, platformOpts =
|
|
|
1339
1403
|
// worktree-branch/cleaned 归因的未提交并入已在 resolveAttributionDiffFiles 内按同口径完成。
|
|
1340
1404
|
if (attribution.mergeGitDir) {
|
|
1341
1405
|
try {
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
//
|
|
1348
|
-
//
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
if (wtGitDir !== cwd) {
|
|
1354
|
-
try {
|
|
1355
|
-
const mainHead = runGit(cwd, ['rev-parse', 'HEAD'])
|
|
1356
|
-
const wtHead = runGit(wtGitDir, ['rev-parse', 'HEAD'])
|
|
1357
|
-
const mb = (mainHead && wtHead) ? runGit(wtGitDir, ['merge-base', wtHead, mainHead]) : null
|
|
1358
|
-
if (mb) {
|
|
1359
|
-
committedFiles = String(runGit(wtGitDir, ['diff', '--name-only', mb, wtHead]) || '')
|
|
1360
|
-
.split('\n').map(p => p.replace(/^"|"$/g, '').replace(/\\/g, '/').trim())
|
|
1361
|
-
.filter(p => p && p !== '.sillyspec' && !p.startsWith('.sillyspec/'))
|
|
1362
|
-
}
|
|
1363
|
-
} catch { /* 已提交补齐失败退回 status-only(fail-open,不拖垮未提交并入) */ }
|
|
1364
|
-
}
|
|
1365
|
-
if (wtFiles.length > 0 || committedFiles.length > 0) {
|
|
1366
|
-
diffFiles = [...new Set([...(Array.isArray(diffFiles) ? diffFiles : []), ...wtFiles, ...committedFiles])]
|
|
1367
|
-
console.log(`[sillyspec] 草稿归属并入 worktree 改动 ${wtFiles.length + committedFiles.length} 个(未提交 ${wtFiles.length} + 已提交 ${committedFiles.length},按 allowed_paths 归属)`)
|
|
1406
|
+
// D-004@v1 口径单一化(坑④ worktree-tick-guard-commit-only,2026-09-15 实证):原手写
|
|
1407
|
+
// 并入段(porcelain 未提交 + committed merge-base 补齐两段)收口为
|
|
1408
|
+
// collectWorktreeChangedFiles 单一真相——勾选守卫 prefetchDiffFileSet(run/complete.js)
|
|
1409
|
+
// 与本草稿归属自此消费同一口径(原「两处口径须同步改」注记就此作废,单一真相在 helper)。
|
|
1410
|
+
// in-place(mergeGitDir===cwd)时 helper 同取 cwd porcelain(现状主仓并入不丢)。
|
|
1411
|
+
// resolveVerifyChangedFiles(verify-postcheck.js)内另有同型补齐段(form A 专属,滤网保
|
|
1412
|
+
// .sillyspec/docs/** 交付物),已知残留 R-09,本次不动。
|
|
1413
|
+
const mergedFiles = collectWorktreeChangedFiles(cwd, changeName, attribution.meta)
|
|
1414
|
+
if (mergedFiles.length > 0) {
|
|
1415
|
+
diffFiles = [...new Set([...(Array.isArray(diffFiles) ? diffFiles : []), ...mergedFiles])]
|
|
1416
|
+
console.log(`[sillyspec] 草稿归属并入 worktree 改动 ${mergedFiles.length} 个(未提交 porcelain + 已提交 merge-base 补齐,按 allowed_paths 归属)`)
|
|
1368
1417
|
}
|
|
1369
1418
|
} catch { /* working-tree 并入失败退回 commit diff 口径(fail-open,不阻断草稿) */ }
|
|
1370
1419
|
}
|