sillyspec 3.28.3 → 3.28.5
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-auto/SKILL.md +54 -54
- package/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -114
- package/.claude/skills/sillyspec-continue/SKILL.md +17 -17
- package/.claude/skills/sillyspec-execute/SKILL.md +222 -222
- package/.claude/skills/sillyspec-knowledge/SKILL.md +40 -40
- package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
- package/.claude/skills/sillyspec-propose/SKILL.md +16 -16
- package/.claude/skills/sillyspec-resume/SKILL.md +21 -21
- package/.claude/skills/sillyspec-state/SKILL.md +25 -25
- package/.claude/skills/sillyspec-workspace/SKILL.md +33 -33
- package/AGENTS.md +25 -0
- package/INSTRUCTIONS.md +12 -0
- package/package.json +1 -1
- package/results-review-tier.md +18 -0
- package/src/agent-session-log.js +4 -3
- package/src/archive-delta.js +82 -0
- package/src/change-delete.js +2 -1
- package/src/change-list.js +18 -3
- package/src/change-risk-profile.js +77 -6
- package/src/commit-suggest.js +52 -1
- package/src/config-cat.js +17 -1
- package/src/constants.js +3 -1
- package/src/decision-distill.js +45 -5
- package/src/decisions-io.js +96 -0
- package/src/design-facts.js +3 -0
- package/src/dispatch/probe.js +31 -8
- package/src/docs-check.js +102 -3
- package/src/doctor-diagnostics.js +441 -5
- package/src/foreign-declared.js +53 -2
- package/src/index.js +606 -22
- package/src/mcp-server.js +144 -0
- package/src/module-impact.js +84 -12
- package/src/modules.js +46 -34
- package/src/plan-adopt-waves.js +13 -5
- package/src/progress/change-registry.js +34 -0
- package/src/progress.js +75 -2
- package/src/quick-recommend.js +6 -1
- package/src/quicklog.js +70 -8
- package/src/review-dispatch.js +377 -0
- package/src/run/archive-distill.js +48 -0
- package/src/run/command.js +48 -13
- package/src/run/complete-handlers.js +215 -9
- package/src/run/complete.js +184 -2
- package/src/run/gate-snapshot.js +88 -0
- package/src/run/gates.js +206 -58
- package/src/run/prompt.js +150 -14
- package/src/run/quick-audit.js +38 -13
- package/src/run/shared.js +183 -12
- package/src/run/stage.js +52 -2
- package/src/run/verify-quality-scan.js +336 -0
- package/src/runtime-hygiene.js +84 -0
- package/src/scope-audit.js +735 -0
- package/src/sillyhub-mcp/client.js +58 -3
- package/src/spec-sync.js +63 -8
- package/src/stage-contract.js +83 -3
- package/src/stage-review-checklist.js +44 -0
- package/src/stage-review.js +125 -4
- package/src/stages/archive.js +60 -167
- package/src/stages/brainstorm.js +7 -17
- package/src/stages/doctor.js +41 -515
- package/src/stages/execute.js +13 -10
- package/src/stages/plan-postcheck.js +191 -7
- package/src/stages/plan.js +7 -35
- package/src/stages/quick.js +11 -11
- package/src/stages/verify.js +24 -39
- package/src/sync-noise.js +158 -0
- package/src/sync.js +93 -27
- package/src/task-review.js +133 -17
- package/src/taskcard.js +46 -3
- package/src/validate-artifacts.js +163 -0
- package/src/verify-facts-schema.js +141 -0
- package/src/verify-postcheck.js +347 -29
- package/src/verify-probes.js +275 -24
- package/src/workflow.js +6 -0
- package/src/worktree-apply.js +167 -24
- package/src/worktree.js +5 -1
- package/templates/agents-instruction.md +1 -0
package/src/worktree-apply.js
CHANGED
|
@@ -90,6 +90,61 @@ export function filterDeliverableFiles(files) {
|
|
|
90
90
|
* excludedCount: number // EXCLUDE-DIRTY + EXCLUDE-MISMATCH 数
|
|
91
91
|
* }}
|
|
92
92
|
*/
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* EXCLUDE-DIRTY 重叠文件的自动三方合并(坑 apply-dirty-block-no-merge,2026-09-10 驾驭
|
|
96
|
+
* 小结第六批③,用户实证 daemon.ts 需手工 patch 合并——三仓并发活跃时 cp 清单不可用)。
|
|
97
|
+
*
|
|
98
|
+
* 语义:对每个 dirty∩changed 重叠文件做 git merge-file 三方合并——base = apply 锚点
|
|
99
|
+
* (baseHash)版本、ours = 主仓当前脏版本(并行会话在途)、theirs = worktree 版本(本变更
|
|
100
|
+
* 交付)。**clean 合并才写回主仓**(两侧改动都保留),该文件视为已应用、由调用方从 patch
|
|
101
|
+
* 集剔除;冲突文件不写回、维持原拦截 + rescue 出口(绝不留半合并现场)。审计:
|
|
102
|
+
* mergedDirtyFiles 逐文件留痕 + 每文件一行日志。
|
|
103
|
+
*
|
|
104
|
+
* fail-soft:base 版本取不到(文件在锚点不存在 = 全新文件)/ merge-file 不可用 → 计入
|
|
105
|
+
* conflicts 列表走原拦截路径,不阻断 apply 主流程。
|
|
106
|
+
*
|
|
107
|
+
* @param {{ projectRoot: string, worktreePath: string, baseHash: string }} ctx
|
|
108
|
+
* @param {string[]} overlapFiles 重叠文件(仓库根相对路径)
|
|
109
|
+
* @returns {{ merged: string[], conflicts: string[] }}
|
|
110
|
+
*/
|
|
111
|
+
export function mergeDirtyOverlapThreeWay({ projectRoot, worktreePath, baseHash }, overlapFiles) {
|
|
112
|
+
const merged = []
|
|
113
|
+
const conflicts = []
|
|
114
|
+
const tmpDir = mkdtempSync(join(tmpdir(), 'sillyspec-3way-'))
|
|
115
|
+
try {
|
|
116
|
+
for (const f of overlapFiles) {
|
|
117
|
+
try {
|
|
118
|
+
const baseRaw = git(projectRoot, ['show', `${baseHash}:${f}`], { encoding: 'buffer', timeout: 15000 })
|
|
119
|
+
const wtRaw = readFileSync(join(worktreePath, f))
|
|
120
|
+
const mainRaw = readFileSync(join(projectRoot, f))
|
|
121
|
+
const p = (tag) => join(tmpDir, `${tag}-${Math.random().toString(36).slice(2, 8)}`)
|
|
122
|
+
const baseP = p('base'); const oursP = p('ours'); const theirsP = p('theirs')
|
|
123
|
+
writeFileSync(baseP, baseRaw); writeFileSync(oursP, mainRaw); writeFileSync(theirsP, wtRaw)
|
|
124
|
+
// merge-file <current=ours> <base> <other=theirs> -p:stdout=合并结果;exit 0=clean,>0=冲突数
|
|
125
|
+
let outBuf, code = 0
|
|
126
|
+
try {
|
|
127
|
+
outBuf = git(projectRoot, ['merge-file', '-p', oursP, baseP, theirsP], { encoding: 'buffer', timeout: 15000 })
|
|
128
|
+
} catch (e) {
|
|
129
|
+
code = (typeof e.status === 'number') ? e.status : -1
|
|
130
|
+
outBuf = (e.stdout instanceof Buffer) ? e.stdout : Buffer.alloc(0)
|
|
131
|
+
if (code < 0) { conflicts.push(f); continue } // merge-file 本身不可用 → 原拦截
|
|
132
|
+
}
|
|
133
|
+
if (code === 0) {
|
|
134
|
+
writeFileSync(join(projectRoot, f), outBuf)
|
|
135
|
+
merged.push(f)
|
|
136
|
+
console.log(`🔀 EXCLUDE-DIRTY 三方合并成功(clean): ${f}——主仓在途改动与 worktree 交付均已保留`)
|
|
137
|
+
} else {
|
|
138
|
+
conflicts.push(f)
|
|
139
|
+
}
|
|
140
|
+
} catch { conflicts.push(f) }
|
|
141
|
+
}
|
|
142
|
+
} finally {
|
|
143
|
+
try { rmSync(tmpDir, { recursive: true, force: true }) } catch {}
|
|
144
|
+
}
|
|
145
|
+
return { merged, conflicts }
|
|
146
|
+
}
|
|
147
|
+
|
|
93
148
|
export function generateRescueCommands({ changedFiles, dirtyFiles, hashMismatchFiles, deletedFiles = [], worktreePath, projectRoot }) {
|
|
94
149
|
// 集合归一:dirtyFiles 接受 Set 或数组(调用方 step4.5/5a 可能传任一形态),其余统一数组→Set
|
|
95
150
|
const dirtySet = dirtyFiles instanceof Set ? dirtyFiles : new Set(dirtyFiles || []);
|
|
@@ -217,23 +272,37 @@ function getBlobHashMap(cwd, treeish, files) {
|
|
|
217
272
|
* 跨仓文件不在主仓 worktree 故永不进 diff);跨仓 task 走 no-op(D-009),其 allowed_paths 仅
|
|
218
273
|
* 作 Map 切片返回,供上游(assess / 跨仓 review 校验)按 repo 取用。
|
|
219
274
|
*
|
|
275
|
+
* **specBase 解析(2026-09-08 用户反馈:平台模式 apply allowlist 读本地空目录整批 BLOCKED)**:
|
|
276
|
+
* 优先级 opts.specBase(调用方传 platformOpts.specRoot)> 平台指针(.sillyspec-platform.json
|
|
277
|
+
* 静默读,不触发 resolvePlatformSpecDir 的守卫 warn)> 本地 projectRoot/.sillyspec。平台模式下
|
|
278
|
+
* changes/ 实体在 specRoot,本地目录常为空——旧硬编码 CHANGES_REL 恒读空 → allow 集恒空 →
|
|
279
|
+
* Gate1 整批误拦(sync.js BUG-01 同族)。
|
|
280
|
+
*
|
|
220
281
|
* @param {string} projectRoot - 主仓库根
|
|
221
282
|
* @param {string} changeName - 变更名
|
|
283
|
+
* @param {{ specBase?: string }} [opts] specBase 显式传入(平台模式必传 specRoot)
|
|
222
284
|
* @returns {Map<string, Set<string>>} repoKey → 并集清单 Set(无 design 清单且无 task 卡片时为空 Map)
|
|
223
285
|
*/
|
|
224
|
-
export function resolveApplyAllowSet(projectRoot, changeName) {
|
|
286
|
+
export function resolveApplyAllowSet(projectRoot, changeName, opts = {}) {
|
|
225
287
|
const repoMap = new Map();
|
|
226
288
|
const getOrCreate = (key) => {
|
|
227
289
|
if (!repoMap.has(key)) repoMap.set(key, new Set());
|
|
228
290
|
return repoMap.get(key);
|
|
229
291
|
};
|
|
230
|
-
|
|
292
|
+
const specBase = opts.specBase || _silentPointerSpecRoot(projectRoot) || join(projectRoot, '.sillyspec');
|
|
293
|
+
// 变更目录读侧归档回退(坑 apply-archived-evidence-recycled,2026-09-10 驾驭小结第二批①):
|
|
294
|
+
// archive 收尾对「未 apply 的 worktree」有意保留并期待归档后补 apply,但归档同时把变更目录
|
|
295
|
+
// rename 到 changes/archive/<name>/ ——旧读侧只认活跃路径,归档后补 apply 时 design/tasks
|
|
296
|
+
// 全部失联,allow 集恒空 → Gate1 整批误拦「不在清单」。活跃目录缺 design.md 而(且)归档
|
|
297
|
+
// 目录在 → 回退读归档版(取证自愈,不改归档回收策略)。
|
|
298
|
+
const changeDir = resolveActiveOrArchiveChangeDir(specBase, changeName);
|
|
299
|
+
// design §6 清单归属 main(清单路径相对主仓根;跨仓 task 的 allowed_paths 由 task 卡 repo 切片)
|
|
231
300
|
const mainSet = getOrCreate('main');
|
|
232
301
|
parseFileChangeList(
|
|
233
|
-
join(
|
|
302
|
+
join(changeDir, 'design.md'),
|
|
234
303
|
{ keepSillyspecDocs: true }
|
|
235
304
|
).forEach(p => mainSet.add(p));
|
|
236
|
-
const tasksDir = join(
|
|
305
|
+
const tasksDir = join(changeDir, 'tasks');
|
|
237
306
|
if (existsSync(tasksDir)) {
|
|
238
307
|
for (const tf of readdirSync(tasksDir).filter(f => /^task-\d+\.md$/.test(f))) {
|
|
239
308
|
const content = readFileSync(join(tasksDir, tf), 'utf8');
|
|
@@ -246,6 +315,36 @@ export function resolveApplyAllowSet(projectRoot, changeName) {
|
|
|
246
315
|
return repoMap;
|
|
247
316
|
}
|
|
248
317
|
|
|
318
|
+
/**
|
|
319
|
+
* 变更文档目录读侧解析:活跃 changes/<name> 优先;活跃目录缺 design.md 而归档
|
|
320
|
+
* changes/archive/<name>/design.md 存在 → 返回归档目录(apply 校验在「归档后补 apply」
|
|
321
|
+
* 场景仍能取到 design/tasks 取证,坑 apply-archived-evidence-recycled)。两处皆缺返回
|
|
322
|
+
* 活跃路径原样(调用方 existsSync 自行降级,行为与旧版一致)。行为经 resolveApplyAllowSet
|
|
323
|
+
* 直测(apply-archive-docs-fallback.test.mjs)覆盖。
|
|
324
|
+
* @param {string} specBase
|
|
325
|
+
* @param {string} changeName
|
|
326
|
+
* @returns {string} 变更目录路径(活跃或归档)
|
|
327
|
+
*/
|
|
328
|
+
function resolveActiveOrArchiveChangeDir(specBase, changeName) {
|
|
329
|
+
const active = join(specBase, 'changes', changeName);
|
|
330
|
+
if (existsSync(join(active, 'design.md'))) return active;
|
|
331
|
+
const archived = join(specBase, 'changes', 'archive', changeName);
|
|
332
|
+
if (existsSync(join(archived, 'design.md'))) return archived;
|
|
333
|
+
return active;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// 平台指针静默读(worktree-apply 内部用):只读 <projectRoot>/.sillyspec-platform.json 的
|
|
337
|
+
// specRoot 字段,不做存在性/自指/temp 守卫、不打 warn——allowlist 只需一个「读得到就用」的
|
|
338
|
+
// 基准目录,坏指针/缺字段落回本地(与 sync-noise.js bindSyncNoiseFromCwd 同款零副作用风格)。
|
|
339
|
+
function _silentPointerSpecRoot(projectRoot) {
|
|
340
|
+
try {
|
|
341
|
+
const p = join(projectRoot, '.sillyspec-platform.json');
|
|
342
|
+
if (!existsSync(p)) return null;
|
|
343
|
+
const s = JSON.parse(readFileSync(p, 'utf8').replace(/^\uFEFF/, ''));
|
|
344
|
+
return s && typeof s.specRoot === 'string' && s.specRoot ? s.specRoot : null;
|
|
345
|
+
} catch { return null; }
|
|
346
|
+
}
|
|
347
|
+
|
|
249
348
|
/**
|
|
250
349
|
* 收集最新 execute run 各 task review.json 的 changedFiles 声明 → Map<repoKey, string[]>。
|
|
251
350
|
*
|
|
@@ -257,14 +356,23 @@ export function resolveApplyAllowSet(projectRoot, changeName) {
|
|
|
257
356
|
* collectExecuteChangedFiles 一致:change 戳归属 run、.sillyspec//meta.json 过滤、
|
|
258
357
|
* review.repo 切片(跨仓声明不进 main 集)。读不到 run/review → 空 Map(fail-closed 回退旧行为)。
|
|
259
358
|
*
|
|
359
|
+
* runtimeRoot 解析(2026-09-08 平台模式修复,与 resolveApplyAllowSet 的 specBase 同族):
|
|
360
|
+
* 优先级 opts.runtimeRoot(调用方传 resolveRuntimeRoot(platformOpts, specBase) 产物)>
|
|
361
|
+
* 指针 specRoot/.runtime(静默读)> 本地 projectRoot/.sillyspec/.runtime。平台模式
|
|
362
|
+
* execute-runs 在平台 runtime root,旧硬编码本地恒空 → 第三源恒缺失。
|
|
363
|
+
*
|
|
260
364
|
* @param {string} projectRoot - 主仓库根
|
|
261
365
|
* @param {string} changeName - 变更名
|
|
366
|
+
* @param {{ runtimeRoot?: string }} [opts]
|
|
262
367
|
* @returns {Map<string, string[]>}
|
|
263
368
|
*/
|
|
264
|
-
export function collectReviewDeclaredFiles(projectRoot, changeName) {
|
|
369
|
+
export function collectReviewDeclaredFiles(projectRoot, changeName, opts = {}) {
|
|
265
370
|
const byRepo = new Map();
|
|
266
371
|
try {
|
|
267
|
-
const
|
|
372
|
+
const ptrSpec = _silentPointerSpecRoot(projectRoot);
|
|
373
|
+
const runtimeRoot = opts.runtimeRoot
|
|
374
|
+
|| (ptrSpec ? join(ptrSpec, '.runtime') : null)
|
|
375
|
+
|| join(projectRoot, '.sillyspec', '.runtime');
|
|
268
376
|
const runId = resolveLatestExecuteRunIdWithTasks({ runtimeRoot, changeName });
|
|
269
377
|
if (!runId) return byRepo;
|
|
270
378
|
const runTasksDir = join(runtimeRoot, 'execute-runs', runId, 'tasks');
|
|
@@ -504,10 +612,11 @@ function applyCrossRepoWorktrees(changeName, projectRoot, ctx, { checkOnly = fal
|
|
|
504
612
|
continue;
|
|
505
613
|
}
|
|
506
614
|
|
|
507
|
-
// 清单校验(per-repo 切片 + review 声明切片并入,与主仓 Gate1/3b
|
|
508
|
-
|
|
615
|
+
// 清单校验(per-repo 切片 + review 声明切片并入,与主仓 Gate1/3b 同语义)。
|
|
616
|
+
// specBase 显式传(平台模式 changes/ 在 specRoot;本地模式与旧硬编码同值零回归)。
|
|
617
|
+
const allowMap = resolveApplyAllowSet(projectRoot, changeName, { specBase });
|
|
509
618
|
let allowSet = allowMap.get(repoKey) || new Set();
|
|
510
|
-
const reviewDeclared = collectReviewDeclaredFiles(projectRoot, changeName).get(repoKey);
|
|
619
|
+
const reviewDeclared = collectReviewDeclaredFiles(projectRoot, changeName, { runtimeRoot: join(specBase, '.runtime') }).get(repoKey);
|
|
511
620
|
if (reviewDeclared) {
|
|
512
621
|
const merged = new Set([...allowSet]);
|
|
513
622
|
for (const f of reviewDeclared) {
|
|
@@ -754,15 +863,21 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
|
|
|
754
863
|
}
|
|
755
864
|
|
|
756
865
|
// --- 3. 解析 apply 文件清单(design §6 清单 ∪ plan TaskCard allowed_paths,execute 复盘 c) ---
|
|
866
|
+
// allowlist 基准目录(2026-09-08 平台模式修复):ctx.platformOpts.specRoot 优先(execute
|
|
867
|
+
// ctx 透传),否则 resolveApplyAllowSet 内部静默读平台指针兜底,最后本地——旧硬编码在
|
|
868
|
+
// 平台模式读本地空 changes/ → allow 集恒空 → Gate1 整批误拦。
|
|
869
|
+
const allowSpecBase = (ctx && ctx.platformOpts && ctx.platformOpts.specRoot) || undefined;
|
|
870
|
+
const allowRuntimeRoot = (ctx && ctx.platformOpts && ctx.platformOpts.runtimeRoot)
|
|
871
|
+
|| (allowSpecBase ? join(allowSpecBase, '.runtime') : undefined);
|
|
757
872
|
// resolveApplyAllowSet 返回 Map<repo, Set>(task-05 跨仓切片);主仓 apply 只消费 main 仓 Set
|
|
758
873
|
// (主仓 worktree diff 只含主仓文件,跨仓文件在跨仓仓不进主仓 worktree diff)。跨仓 allowed_paths
|
|
759
874
|
// 仅作 Map 切片返回供上游用,主流程不消费(跨仓 apply=no-op,D-009)。
|
|
760
|
-
const allowMap = resolveApplyAllowSet(projectRoot, changeName);
|
|
875
|
+
const allowMap = resolveApplyAllowSet(projectRoot, changeName, { specBase: allowSpecBase });
|
|
761
876
|
// --- 3b. review.json 声明偏差文件并入(坑 apply-undeclared-deviation-block)---
|
|
762
877
|
// 第三源:reviewer 声明的 changedFiles(已过 Task Review Gate git 证据交叉校验)——执行期
|
|
763
878
|
// 有据越界(facade 转发/名单测试)不再逼回改 design.md。各 repo 切片各自并入;仅靠 review
|
|
764
879
|
// 放行的文件记审计 warning(result.reviewAdmittedFiles),完全越界文件仍拦(Gate1 在扩展后判)。
|
|
765
|
-
const reviewDeclaredByRepo = collectReviewDeclaredFiles(projectRoot, changeName);
|
|
880
|
+
const reviewDeclaredByRepo = collectReviewDeclaredFiles(projectRoot, changeName, { runtimeRoot: allowRuntimeRoot });
|
|
766
881
|
const reviewAdmittedFiles = [];
|
|
767
882
|
for (const [repoKey, files] of reviewDeclaredByRepo) {
|
|
768
883
|
const repoSet = allowMap.get(repoKey);
|
|
@@ -904,27 +1019,50 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
|
|
|
904
1019
|
)].filter(f => !f.startsWith('.sillyspec/') && f !== 'meta.json');
|
|
905
1020
|
const overlapDirty = dirtyFiles.filter(f => changedFiles.includes(f));
|
|
906
1021
|
if (overlapDirty.length > 0) {
|
|
907
|
-
//
|
|
908
|
-
//
|
|
909
|
-
//
|
|
910
|
-
//
|
|
911
|
-
|
|
912
|
-
if (
|
|
913
|
-
|
|
914
|
-
|
|
1022
|
+
// 自动三方合并(坑 apply-dirty-block-no-merge,2026-09-10 驾驭小结第六批③):先对
|
|
1023
|
+
// 重叠文件尝试 merge-file(base=锚点版 ours=主仓脏版 theirs=worktree 版),clean 合并
|
|
1024
|
+
// 写回主仓、该文件视为已应用(从 patch 集剔除);仅剩冲突文件才走原拦截+rescue 出口。
|
|
1025
|
+
// checkOnly(assess 只读)不试合并——探查路径零写盘。
|
|
1026
|
+
let conflictDirty = overlapDirty;
|
|
1027
|
+
if (!checkOnly && deliverableBase && worktreePath && worktreePath !== projectRoot) {
|
|
1028
|
+
try {
|
|
1029
|
+
const m3 = mergeDirtyOverlapThreeWay({ projectRoot, worktreePath, baseHash: deliverableBase }, overlapDirty);
|
|
1030
|
+
if (m3.merged.length > 0) {
|
|
1031
|
+
result.mergedDirtyFiles = (result.mergedDirtyFiles || []).concat(m3.merged);
|
|
1032
|
+
const mergedSet = new Set(m3.merged);
|
|
1033
|
+
changedFiles = changedFiles.filter(f => !mergedSet.has(f));
|
|
1034
|
+
result.changedFiles = changedFiles;
|
|
1035
|
+
result.deletedFiles = deletedFiles.filter(f => !mergedSet.has(f));
|
|
1036
|
+
result.hashMismatchFiles = (result.hashMismatchFiles || []).filter(f => !mergedSet.has(f));
|
|
1037
|
+
conflictDirty = m3.conflicts;
|
|
1038
|
+
} else {
|
|
1039
|
+
conflictDirty = m3.conflicts.length > 0 ? m3.conflicts : overlapDirty;
|
|
1040
|
+
}
|
|
1041
|
+
} catch { conflictDirty = overlapDirty; /* 合并链路异常 → 原拦截 */ }
|
|
1042
|
+
}
|
|
1043
|
+
if (conflictDirty.length === 0) {
|
|
1044
|
+
result.warnings = (result.warnings || []).concat([
|
|
1045
|
+
`EXCLUDE-DIRTY 全部经三方合并消解(${overlapDirty.length} 个文件 clean 合并已写回,主仓在途改动与 worktree 交付均保留)——继续正常 apply 剩余文件`
|
|
1046
|
+
]);
|
|
1047
|
+
} else if (skipOverlap && !checkOnly) {
|
|
1048
|
+
// --skip-overlap(坑 apply-overlap-all-or-nothing,2026-08-23 实证):剔除仍冲突的
|
|
1049
|
+
// 重叠文件(三方合并已消解的不在此列)、应用干净子集。重叠文件留在 worktree——
|
|
1050
|
+
// step8 非 force cleanup 被 hasUnappliedChanges 护栏拦住,待主仓干净后重新 apply 或人工裁决。
|
|
1051
|
+
const skipSet = new Set(conflictDirty);
|
|
1052
|
+
result.skippedOverlapFiles = conflictDirty.slice();
|
|
915
1053
|
changedFiles = changedFiles.filter(f => !skipSet.has(f));
|
|
916
1054
|
result.changedFiles = changedFiles;
|
|
917
1055
|
result.deletedFiles = deletedFiles.filter(f => !skipSet.has(f));
|
|
918
1056
|
result.hashMismatchFiles = (result.hashMismatchFiles || []).filter(f => !skipSet.has(f));
|
|
919
1057
|
if (changedFiles.length === 0) {
|
|
920
1058
|
result.errors.push(
|
|
921
|
-
`--skip-overlap:本次 ${
|
|
1059
|
+
`--skip-overlap:本次 ${conflictDirty.length} 个变更文件与主仓未提交改动重叠且无法自动三方合并,无可应用子集:\n ${conflictDirty.join('\n ')}\n` +
|
|
922
1060
|
`请先提交/stash 主仓改动后重试(不带 --skip-overlap),或 sillyspec worktree apply ${changeName} --stash-dirty(工具内置 stash→apply→恢复,保暂存区状态,SHA 兜底可审计)。`
|
|
923
1061
|
);
|
|
924
1062
|
return result;
|
|
925
1063
|
}
|
|
926
1064
|
result.warnings = (result.warnings || []).concat([
|
|
927
|
-
`--skip-overlap:跳过 ${
|
|
1065
|
+
`--skip-overlap:跳过 ${conflictDirty.length} 个与主仓未提交改动重叠的文件(留在 worktree,未应用):${conflictDirty.join(', ')}——待主仓提交/stash 后重新 apply 只应用剩余文件,或确认放弃后 cleanup --force`
|
|
928
1066
|
]);
|
|
929
1067
|
} else {
|
|
930
1068
|
// 重叠拦截:只有与本次变更同文件的未提交改动才无法安全 apply(列重叠文件,非全部脏文件)
|
|
@@ -938,7 +1076,7 @@ export function applyWorktree(changeName, { cwd, checkOnly = false, merge = fals
|
|
|
938
1076
|
projectRoot,
|
|
939
1077
|
});
|
|
940
1078
|
result.errors.push(
|
|
941
|
-
`主工作区以下未提交文件与本次 apply
|
|
1079
|
+
`主工作区以下未提交文件与本次 apply 的变更重叠,自动三方合并后仍冲突,无法安全应用:\n ${conflictDirty.join('\n ')}\n` +
|
|
942
1080
|
`请先提交或暂存这些改动,再重新 apply:\n git add -A && git commit -m "..." 或 git stash\n` +
|
|
943
1081
|
`或 sillyspec worktree apply ${changeName} --stash-dirty(工具内置 stash→apply→恢复,保暂存区状态,SHA 兜底可审计)\n` +
|
|
944
1082
|
`或应用非重叠部分(重叠文件留在 worktree,主仓干净后重新 apply 只补剩余):\n sillyspec worktree apply ${changeName} --skip-overlap\n` +
|
|
@@ -1693,13 +1831,18 @@ export function assessApplyRisk(changeName, { cwd } = {}) {
|
|
|
1693
1831
|
|
|
1694
1832
|
// design §6 标记为「顺带修复」的文件(坑 worktree-execute-apply-friction 坑1):合规修预存债,
|
|
1695
1833
|
// 不属任何 task 边界,assess 豁免 allowed_paths 严格校验(降级 warning),避免被迫 cherry-pick 绕过。
|
|
1696
|
-
|
|
1834
|
+
// specBase 走指针/本地解析(2026-09-08 平台模式修复,与 resolveApplyAllowSet 同族)。
|
|
1835
|
+
// 归档回退(坑 apply-archived-evidence-recycled):归档后补 apply 时变更目录已 rename,
|
|
1836
|
+
// design/tasks 从 changes/archive/<name>/ 读(与 resolveApplyAllowSet 同口径)。
|
|
1837
|
+
const assessSpecBase = _silentPointerSpecRoot(projectRoot) || join(projectRoot, '.sillyspec');
|
|
1838
|
+
const assessChangeDir = resolveActiveOrArchiveChangeDir(assessSpecBase, changeName);
|
|
1839
|
+
const designPath = join(assessChangeDir, 'design.md');
|
|
1697
1840
|
const incidentalSet = new Set(
|
|
1698
1841
|
parseFileChangeListDetailed(designPath).filter(e => e.incidental).map(e => e.path)
|
|
1699
1842
|
);
|
|
1700
1843
|
// review.json 声明偏差文件(坑 apply-undeclared-deviation-block):与顺带修复同等待遇——
|
|
1701
1844
|
// reviewer 声明(已过 git 证据校验)的执行期偏差豁免 allowed_paths 严格校验,降 warning 注明来源。
|
|
1702
|
-
const reviewDeclaredSet = new Set(collectReviewDeclaredFiles(projectRoot, changeName).get('main') || []);
|
|
1845
|
+
const reviewDeclaredSet = new Set(collectReviewDeclaredFiles(projectRoot, changeName, { runtimeRoot: join(assessSpecBase, '.runtime') }).get('main') || []);
|
|
1703
1846
|
|
|
1704
1847
|
// 检查 2: 变更在 allowed_paths 内(仅在 TaskCard 存在时);顺带修复/review 声明文件豁免。
|
|
1705
1848
|
// 匹配换 pathMatches(与 Gate1/plan-postcheck 同语义容差),消除原字面前缀弱匹配漂移。
|
package/src/worktree.js
CHANGED
|
@@ -203,7 +203,11 @@ function printSyncReport(diag, baseHash, changeName) {
|
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
function parseJSON(raw) {
|
|
206
|
-
|
|
206
|
+
// BOM 容错(2026-09-08 用户反馈:meta.json 带 UTF-8 BOM 被当损坏 → apply 报
|
|
207
|
+
// 「meta.json 不存在或已损坏」、幽灵判定误清):Windows 编辑器(记事本/部分 PowerShell
|
|
208
|
+
// 重定向)写 JSON 常带 \uFEFF 前缀,JSON.parse 对首字符 BOM 直接抛。剥掉再 parse。
|
|
209
|
+
const s = typeof raw === 'string' ? raw.replace(/^\uFEFF/, '') : raw
|
|
210
|
+
try { return JSON.parse(s); } catch { return null; }
|
|
207
211
|
}
|
|
208
212
|
|
|
209
213
|
/**
|
|
@@ -21,3 +21,4 @@
|
|
|
21
21
|
15. **quicklog 结构化落盘**:末步 `--done` 用四参数 `--req/--cause/--solution/--result`(CLI 合成结构化 output 并自动提取标题/四段分行);文件括注用 `--file-notes "path::注 || path2"`。骨架由 CLI 接管,`--done` 后按需核对即可,勿手拼模板。
|
|
22
22
|
16. **代码可能随时在修改**(多 agent 并行),Edit 前重跑 + 查最新态;破坏性 git op 前先备份。
|
|
23
23
|
17. **不奉承用户**,禁止"你说得对"类话术,直接给结论、依据、方案。
|
|
24
|
+
18. **禁止目录级 git add / git add -A**(多会话共享仓):.sillyspec/ 等目录里有并行会话的进行中文件与已提交文档,目录级暂存会夹带他者改动甚至误删已提交文件(2026-09-10 实证)。提交一律用显式 pathspec(git add -- 文件1 文件2),提交前 git status 核对暂存面只含本会话文件。
|