sillyspec 3.25.0 → 3.25.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -661,7 +661,7 @@ export function checkExecuteCodeEvidence(cwd, changeName) {
661
661
  * 防止"勾选 checkbox = 完成 execute"的谎报路径。
662
662
  */
663
663
  function validateExecuteOutputs(cwd, changeName, context = {}) {
664
- const { specRoot } = context
664
+ const { specRoot, evidence: passedEvidence } = context
665
665
  const errors = []
666
666
  const warnings = []
667
667
 
@@ -674,7 +674,9 @@ function validateExecuteOutputs(cwd, changeName, context = {}) {
674
674
  const hasTasks = /^\s*[-*]\s*\[[ xX]\]\s*task-\d+/m.test(planContent)
675
675
  if (!hasTasks) return { ok: true, errors, warnings }
676
676
 
677
- const evidence = checkExecuteCodeEvidence(cwd, changeName)
677
+ // W4-G (D-008):evidence 可由调用方(runGate)预先算好传入,避免与 execute-evidence check
678
+ // 重复 spawn git(gate execute 一次省 2 个 git 进程 ≈ 60-200ms on Windows)。未传则内部算(向后兼容)。
679
+ const evidence = passedEvidence || checkExecuteCodeEvidence(cwd, changeName)
678
680
  if (evidence.status === 'unchanged') {
679
681
  errors.push(`execute 代码变更核验失败:plan.md 声明了任务,但 ${evidence.detail} — 勾选 checkbox 不等于完成实现`)
680
682
  } else if (evidence.status === 'unknown') {
package/src/worktree.js CHANGED
@@ -13,6 +13,11 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, rmSync, readdirSync
13
13
  import { join, resolve, dirname } from 'path';
14
14
  import { createHash } from 'crypto';
15
15
  import { provisionDeps, lockfileHash } from './worktree-deps.js';
16
+ import { writeAtomicSync } from './fs-atomic.js';
17
+
18
+ // meta.json 会被 hook 进程与其它 CLI 进程并发读取(worktree-guard / getMeta / create 幽灵判定),
19
+ // 必须原子写:半截 JSON 会让 getMeta 返回 null → 触发幽灵 worktree 强删(可能丢 gitignored 改动)。
20
+ const writeMetaAtomic = (metaPath, meta) => writeAtomicSync(metaPath, JSON.stringify(meta, null, 2) + '\n');
16
21
 
17
22
  const WORKTREES_REL = '.sillyspec/.runtime/worktrees';
18
23
  const BRANCH_PREFIX = 'sillyspec/';
@@ -414,8 +419,12 @@ export class WorktreeManager {
414
419
  createdAt: new Date().toISOString(),
415
420
  provisioning: true,
416
421
  };
417
- writeFileSync(join(worktreePath, META_FILE), JSON.stringify(placeholderMeta, null, 2) + '\n');
418
- } catch {}
422
+ writeMetaAtomic(join(worktreePath, META_FILE), placeholderMeta);
423
+ } catch (e) {
424
+ // 占位 meta 是「防 create 中断 → 幽灵强删」的守卫。写失败不能静默:否则中断后
425
+ // getMeta 仍返回 null,下次 create 会把含 gitignored 改动的 worktree 当幽灵删掉。
426
+ console.warn(`⚠️ 占位 meta 写入失败: ${e.message}(若 create 中断,下次可能误判幽灵 worktree)`);
427
+ }
419
428
 
420
429
  // 5.5 base 同步检测(只读:不 ff、不改 HEAD;best-effort fetch 失败降级用缓存)。
421
430
  // 旧逻辑 fetch + merge --ff-only:分叉时 ff 失败被静默吞(syncStatus=failed),成功时
@@ -468,7 +477,7 @@ export class WorktreeManager {
468
477
  };
469
478
 
470
479
  const metaPath = join(worktreePath, META_FILE);
471
- writeFileSync(metaPath, JSON.stringify(meta, null, 2) + '\n');
480
+ writeMetaAtomic(metaPath, meta);
472
481
 
473
482
  return { branch, worktreePath, baseHash, mode: meta.mode, syncDiagnostic };
474
483
  }
@@ -499,7 +508,7 @@ export class WorktreeManager {
499
508
  if (!existsSync(this.worktreeBase)) mkdirSync(this.worktreeBase, { recursive: true })
500
509
  const metaDir = join(this.worktreeBase, name)
501
510
  if (!existsSync(metaDir)) mkdirSync(metaDir, { recursive: true })
502
- writeFileSync(join(metaDir, META_FILE), JSON.stringify(meta, null, 2) + '\n')
511
+ writeMetaAtomic(join(metaDir, META_FILE), meta)
503
512
  console.log(`🔗 native-worktree meta 已恢复: ${metaDir}/meta.json`)
504
513
  return { branch: meta.branch, worktreePath, baseHash, mode: meta.mode }
505
514
  }
@@ -552,7 +561,7 @@ export class WorktreeManager {
552
561
  if (!existsSync(this.worktreeBase)) mkdirSync(this.worktreeBase, { recursive: true })
553
562
  const metaDir = join(this.worktreeBase, name)
554
563
  if (!existsSync(metaDir)) mkdirSync(metaDir, { recursive: true })
555
- writeFileSync(join(metaDir, META_FILE), JSON.stringify(meta, null, 2) + '\n')
564
+ writeMetaAtomic(join(metaDir, META_FILE), meta)
556
565
  return { branch: meta.branch, worktreePath, baseHash, mode: meta.mode }
557
566
  }
558
567
 
@@ -596,7 +605,7 @@ export class WorktreeManager {
596
605
  if (!existsSync(metaDir)) {
597
606
  mkdirSync(metaDir, { recursive: true });
598
607
  }
599
- writeFileSync(metaPath, JSON.stringify(meta, null, 2) + '\n');
608
+ writeMetaAtomic(metaPath, meta);
600
609
 
601
610
  return { branch: meta.branch, worktreePath, baseHash, mode: meta.mode };
602
611
  }
@@ -817,7 +826,7 @@ export class WorktreeManager {
817
826
  const deps = provisionDeps(wtPath, this.cwd, { specBase: join(this.cwd, '.sillyspec') }) || {};
818
827
  const metaPath = join(this.getWorktreePath(name), META_FILE);
819
828
  const meta = this.getMeta(name) || {};
820
- writeFileSync(metaPath, JSON.stringify({ ...meta, ...deps }, null, 2) + '\n');
829
+ writeMetaAtomic(metaPath, { ...meta, ...deps });
821
830
  return { ok: true, msg: `re-provisioned ${name}: depsStatus=${deps.depsStatus}` };
822
831
  } catch (e) {
823
832
  return { ok: false, msg: `re-provision failed for ${name}: ${e.message}` };
@@ -1094,7 +1103,7 @@ export class WorktreeManager {
1094
1103
  const dst = join(worktreePath, f);
1095
1104
  if (existsSync(src)) {
1096
1105
  mkdirSync(dirname(dst), { recursive: true });
1097
- try { writeFileSync(dst, readFileSync(src)); files.push(f); } catch {}
1106
+ try { writeFileSync(dst, readFileSync(src)); files.push(f); } catch (e) { errors.push(`untracked ${f}: ${e.message}`); }
1098
1107
  }
1099
1108
  }
1100
1109
  }