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.
Files changed (77) hide show
  1. package/.claude/skills/sillyspec-auto/SKILL.md +54 -54
  2. package/.claude/skills/sillyspec-brainstorm/SKILL.md +114 -114
  3. package/.claude/skills/sillyspec-continue/SKILL.md +17 -17
  4. package/.claude/skills/sillyspec-execute/SKILL.md +222 -222
  5. package/.claude/skills/sillyspec-knowledge/SKILL.md +40 -40
  6. package/.claude/skills/sillyspec-plan/SKILL.md +156 -156
  7. package/.claude/skills/sillyspec-propose/SKILL.md +16 -16
  8. package/.claude/skills/sillyspec-resume/SKILL.md +21 -21
  9. package/.claude/skills/sillyspec-state/SKILL.md +25 -25
  10. package/.claude/skills/sillyspec-workspace/SKILL.md +33 -33
  11. package/AGENTS.md +25 -0
  12. package/INSTRUCTIONS.md +12 -0
  13. package/package.json +1 -1
  14. package/results-review-tier.md +18 -0
  15. package/src/agent-session-log.js +4 -3
  16. package/src/archive-delta.js +82 -0
  17. package/src/change-delete.js +2 -1
  18. package/src/change-list.js +18 -3
  19. package/src/change-risk-profile.js +77 -6
  20. package/src/commit-suggest.js +52 -1
  21. package/src/config-cat.js +17 -1
  22. package/src/constants.js +3 -1
  23. package/src/decision-distill.js +45 -5
  24. package/src/decisions-io.js +96 -0
  25. package/src/design-facts.js +3 -0
  26. package/src/dispatch/probe.js +31 -8
  27. package/src/docs-check.js +102 -3
  28. package/src/doctor-diagnostics.js +441 -5
  29. package/src/foreign-declared.js +53 -2
  30. package/src/index.js +606 -22
  31. package/src/mcp-server.js +144 -0
  32. package/src/module-impact.js +84 -12
  33. package/src/modules.js +46 -34
  34. package/src/plan-adopt-waves.js +13 -5
  35. package/src/progress/change-registry.js +34 -0
  36. package/src/progress.js +75 -2
  37. package/src/quick-recommend.js +6 -1
  38. package/src/quicklog.js +70 -8
  39. package/src/review-dispatch.js +377 -0
  40. package/src/run/archive-distill.js +48 -0
  41. package/src/run/command.js +48 -13
  42. package/src/run/complete-handlers.js +215 -9
  43. package/src/run/complete.js +184 -2
  44. package/src/run/gate-snapshot.js +88 -0
  45. package/src/run/gates.js +206 -58
  46. package/src/run/prompt.js +150 -14
  47. package/src/run/quick-audit.js +38 -13
  48. package/src/run/shared.js +183 -12
  49. package/src/run/stage.js +52 -2
  50. package/src/run/verify-quality-scan.js +336 -0
  51. package/src/runtime-hygiene.js +84 -0
  52. package/src/scope-audit.js +735 -0
  53. package/src/sillyhub-mcp/client.js +58 -3
  54. package/src/spec-sync.js +63 -8
  55. package/src/stage-contract.js +83 -3
  56. package/src/stage-review-checklist.js +44 -0
  57. package/src/stage-review.js +125 -4
  58. package/src/stages/archive.js +60 -167
  59. package/src/stages/brainstorm.js +7 -17
  60. package/src/stages/doctor.js +41 -515
  61. package/src/stages/execute.js +13 -10
  62. package/src/stages/plan-postcheck.js +191 -7
  63. package/src/stages/plan.js +7 -35
  64. package/src/stages/quick.js +11 -11
  65. package/src/stages/verify.js +24 -39
  66. package/src/sync-noise.js +158 -0
  67. package/src/sync.js +93 -27
  68. package/src/task-review.js +133 -17
  69. package/src/taskcard.js +46 -3
  70. package/src/validate-artifacts.js +163 -0
  71. package/src/verify-facts-schema.js +141 -0
  72. package/src/verify-postcheck.js +347 -29
  73. package/src/verify-probes.js +275 -24
  74. package/src/workflow.js +6 -0
  75. package/src/worktree-apply.js +167 -24
  76. package/src/worktree.js +5 -1
  77. package/templates/agents-instruction.md +1 -0
@@ -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
- // design §6 清单归属 main(清单路径相对主仓根;跨仓 task 的 allowed_paths 由 task 卡片 repo 切片)
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(projectRoot, CHANGES_REL, changeName, 'design.md'),
302
+ join(changeDir, 'design.md'),
234
303
  { keepSillyspecDocs: true }
235
304
  ).forEach(p => mainSet.add(p));
236
- const tasksDir = join(projectRoot, CHANGES_REL, changeName, 'tasks');
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 runtimeRoot = join(projectRoot, '.sillyspec', '.runtime');
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
- const allowMap = resolveApplyAllowSet(projectRoot, changeName);
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
- // --skip-overlap(坑 apply-overlap-all-or-nothing,2026-08-23 实证:多 agent 并发仓主仓
908
- // 常态有在途变更,overlap 整批拦截使 apply 基本不可用,rescue 手动 cp 又留混合状态):
909
- // 显式 opt-in 剔除重叠文件、应用干净子集。重叠文件留在 worktree——step8 非 force
910
- // cleanup 被 hasUnappliedChanges 护栏拦住(主仓工作区逐字节降噪层只剔除已应用文件),
911
- // 待主仓干净后重新 apply(此时只剩剩余文件)或人工裁决。
912
- if (skipOverlap && !checkOnly) {
913
- const skipSet = new Set(overlapDirty);
914
- result.skippedOverlapFiles = overlapDirty.slice();
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:本次 ${overlapDirty.length} 个变更文件全部与主仓未提交改动重叠,无可应用子集:\n ${overlapDirty.join('\n ')}\n` +
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:跳过 ${overlapDirty.length} 个与主仓未提交改动重叠的文件(留在 worktree,未应用):${overlapDirty.join(', ')}——待主仓提交/stash 后重新 apply 只应用剩余文件,或确认放弃后 cleanup --force`
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 的变更重叠,git apply 无法安全应用:\n ${overlapDirty.join('\n ')}\n` +
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
- const designPath = join(projectRoot, CHANGES_REL, changeName, 'design.md');
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
- try { return JSON.parse(raw); } catch { return null; }
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 核对暂存面只含本会话文件。