sillyspec 3.25.0 → 3.25.2

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 (45) hide show
  1. package/.claude/skills/sillyspec-doctor/SKILL.md +9 -0
  2. package/.playwright-mcp/console-2026-07-27T15-45-50-621Z.log +1 -0
  3. package/.playwright-mcp/page-2026-07-27T15-45-51-459Z.yml +144 -0
  4. package/.playwright-mcp/prompt-final.png +0 -0
  5. package/.playwright-mcp/prompt-view.png +0 -0
  6. package/CLAUDE.md +18 -0
  7. package/package.json +1 -1
  8. package/src/change-risk-profile.js +60 -12
  9. package/src/check-primitives.js +65 -0
  10. package/src/db.js +28 -8
  11. package/src/hooks/worktree-guard.js +9 -2
  12. package/src/index.js +140 -11
  13. package/src/machine-interface.js +13 -7
  14. package/src/progress/change-registry.js +217 -0
  15. package/src/progress/consistency-doctor.js +259 -0
  16. package/src/progress/shared.js +33 -0
  17. package/src/progress/stage-machine.js +535 -0
  18. package/src/progress/step-store.js +256 -0
  19. package/src/progress.js +75 -1191
  20. package/src/quicklog.js +3 -1
  21. package/src/run/command.js +1088 -0
  22. package/src/run/complete-handlers.js +874 -0
  23. package/src/run/complete.js +725 -0
  24. package/src/run/gates.js +326 -0
  25. package/src/run/prompt.js +619 -0
  26. package/src/run/quick-audit.js +107 -0
  27. package/src/run/scan-profile.js +238 -0
  28. package/src/run/shared.js +418 -0
  29. package/src/run/stage.js +414 -0
  30. package/src/run.js +23 -4379
  31. package/src/stage-contract-engine.js +150 -0
  32. package/src/stage-contract-spec.js +517 -0
  33. package/src/stage-contract.js +87 -150
  34. package/src/stage-review.js +63 -0
  35. package/src/stages/brainstorm.js +3 -1
  36. package/src/stages/execute.js +5 -2
  37. package/src/stages/plan-postcheck.js +38 -27
  38. package/src/stages/plan.js +12 -49
  39. package/src/stages/propose.js +2 -1
  40. package/src/stages/scan.js +3 -1
  41. package/src/workflow.js +9 -17
  42. package/src/worktree-apply.js +5 -2
  43. package/src/worktree-deps.js +114 -32
  44. package/src/worktree.js +23 -11
  45. package/src/brainstorm-postcheck.js +0 -158
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/';
@@ -162,9 +167,12 @@ function sleepMs(ms) {
162
167
  while (Date.now() < end) { /* spin */ }
163
168
  }
164
169
 
165
- function computeBaselineHash(cwd) {
166
- // 排除 .sillyspec/ 元数据目录,避免 brainstorm/plan 阶段修改的蓝图文件污染 baseline
167
- const exclude = '-- . ":(exclude).sillyspec/"';
170
+ export function computeBaselineHash(cwd) {
171
+ // 排除 execute 流程自身会改的元数据,避免漂移误报(否则每个 execute 收尾都因自身改动 BLOCKED):
172
+ // - .sillyspec/:brainstorm/plan 蓝图 + runtime 产物
173
+ // - docs/sillyspec/:工具坑文件(CLAUDE.md 规则要求 execute 期间记录)
174
+ // 必须和 applyWorktree step 4.5 (worktree-apply.js) 使用相同的排除规则。
175
+ const exclude = '-- . ":(exclude).sillyspec/" ":(exclude)docs/sillyspec/"';
168
176
  const staged = gitQuiet(cwd, `diff --cached ${exclude}`) || '';
169
177
  const unstaged = gitQuiet(cwd, `diff ${exclude}`) || '';
170
178
  const untracked = gitQuiet(cwd, `ls-files --others --exclude-standard ${exclude}`) || '';
@@ -414,8 +422,12 @@ export class WorktreeManager {
414
422
  createdAt: new Date().toISOString(),
415
423
  provisioning: true,
416
424
  };
417
- writeFileSync(join(worktreePath, META_FILE), JSON.stringify(placeholderMeta, null, 2) + '\n');
418
- } catch {}
425
+ writeMetaAtomic(join(worktreePath, META_FILE), placeholderMeta);
426
+ } catch (e) {
427
+ // 占位 meta 是「防 create 中断 → 幽灵强删」的守卫。写失败不能静默:否则中断后
428
+ // getMeta 仍返回 null,下次 create 会把含 gitignored 改动的 worktree 当幽灵删掉。
429
+ console.warn(`⚠️ 占位 meta 写入失败: ${e.message}(若 create 中断,下次可能误判幽灵 worktree)`);
430
+ }
419
431
 
420
432
  // 5.5 base 同步检测(只读:不 ff、不改 HEAD;best-effort fetch 失败降级用缓存)。
421
433
  // 旧逻辑 fetch + merge --ff-only:分叉时 ff 失败被静默吞(syncStatus=failed),成功时
@@ -468,7 +480,7 @@ export class WorktreeManager {
468
480
  };
469
481
 
470
482
  const metaPath = join(worktreePath, META_FILE);
471
- writeFileSync(metaPath, JSON.stringify(meta, null, 2) + '\n');
483
+ writeMetaAtomic(metaPath, meta);
472
484
 
473
485
  return { branch, worktreePath, baseHash, mode: meta.mode, syncDiagnostic };
474
486
  }
@@ -499,7 +511,7 @@ export class WorktreeManager {
499
511
  if (!existsSync(this.worktreeBase)) mkdirSync(this.worktreeBase, { recursive: true })
500
512
  const metaDir = join(this.worktreeBase, name)
501
513
  if (!existsSync(metaDir)) mkdirSync(metaDir, { recursive: true })
502
- writeFileSync(join(metaDir, META_FILE), JSON.stringify(meta, null, 2) + '\n')
514
+ writeMetaAtomic(join(metaDir, META_FILE), meta)
503
515
  console.log(`🔗 native-worktree meta 已恢复: ${metaDir}/meta.json`)
504
516
  return { branch: meta.branch, worktreePath, baseHash, mode: meta.mode }
505
517
  }
@@ -552,7 +564,7 @@ export class WorktreeManager {
552
564
  if (!existsSync(this.worktreeBase)) mkdirSync(this.worktreeBase, { recursive: true })
553
565
  const metaDir = join(this.worktreeBase, name)
554
566
  if (!existsSync(metaDir)) mkdirSync(metaDir, { recursive: true })
555
- writeFileSync(join(metaDir, META_FILE), JSON.stringify(meta, null, 2) + '\n')
567
+ writeMetaAtomic(join(metaDir, META_FILE), meta)
556
568
  return { branch: meta.branch, worktreePath, baseHash, mode: meta.mode }
557
569
  }
558
570
 
@@ -596,7 +608,7 @@ export class WorktreeManager {
596
608
  if (!existsSync(metaDir)) {
597
609
  mkdirSync(metaDir, { recursive: true });
598
610
  }
599
- writeFileSync(metaPath, JSON.stringify(meta, null, 2) + '\n');
611
+ writeMetaAtomic(metaPath, meta);
600
612
 
601
613
  return { branch: meta.branch, worktreePath, baseHash, mode: meta.mode };
602
614
  }
@@ -817,7 +829,7 @@ export class WorktreeManager {
817
829
  const deps = provisionDeps(wtPath, this.cwd, { specBase: join(this.cwd, '.sillyspec') }) || {};
818
830
  const metaPath = join(this.getWorktreePath(name), META_FILE);
819
831
  const meta = this.getMeta(name) || {};
820
- writeFileSync(metaPath, JSON.stringify({ ...meta, ...deps }, null, 2) + '\n');
832
+ writeMetaAtomic(metaPath, { ...meta, ...deps });
821
833
  return { ok: true, msg: `re-provisioned ${name}: depsStatus=${deps.depsStatus}` };
822
834
  } catch (e) {
823
835
  return { ok: false, msg: `re-provision failed for ${name}: ${e.message}` };
@@ -1094,7 +1106,7 @@ export class WorktreeManager {
1094
1106
  const dst = join(worktreePath, f);
1095
1107
  if (existsSync(src)) {
1096
1108
  mkdirSync(dirname(dst), { recursive: true });
1097
- try { writeFileSync(dst, readFileSync(src)); files.push(f); } catch {}
1109
+ try { writeFileSync(dst, readFileSync(src)); files.push(f); } catch (e) { errors.push(`untracked ${f}: ${e.message}`); }
1098
1110
  }
1099
1111
  }
1100
1112
  }
@@ -1,158 +0,0 @@
1
- /**
2
- * brainstorm-postcheck.js — plan 阶段对 brainstorm 产物的校验
3
- *
4
- * 在 brainstorm → plan 之间执行,校验 brainstorm 产物完整性。
5
- * PASS → 继续 plan
6
- * WARN → 继续 plan,记录警告并分配补齐任务
7
- * FAIL → 回退到 brainstorm 补齐(最多 2 次重试)
8
- */
9
-
10
- import { existsSync, readFileSync } from 'fs'
11
- import { join } from 'path'
12
-
13
- /**
14
- * 校验 brainstorm 产物完整性
15
- * @param {string} changeDir - 变更目录路径(.sillyspec/changes/<change>)
16
- * @returns {{ ok: boolean, level: 'PASS'|'WARN'|'FAIL', errors: string[], warnings: string[] }}
17
- */
18
- export function checkBrainstormArtifacts(changeDir) {
19
- const errors = []
20
- const warnings = []
21
- const brainstormDir = join(changeDir, 'brainstorm')
22
-
23
- // ── 1. 产物完整性检查 ──
24
- const requiredFiles = ['design.md', 'decisions.md', 'gaps.md', 'assumptions.md', 'next-action.json']
25
- for (const file of requiredFiles) {
26
- if (!existsSync(join(brainstormDir, file))) {
27
- errors.push(`brainstorm/${file} 不存在`)
28
- }
29
- }
30
-
31
- // ── 2. next-action.json 有效性检查 ──
32
- const nextActionFile = join(brainstormDir, 'next-action.json')
33
- if (existsSync(nextActionFile)) {
34
- try {
35
- const nextAction = JSON.parse(readFileSync(nextActionFile, 'utf8'))
36
-
37
- // 必填字段
38
- if (!['ready_for_plan', 'waiting_for_user'].includes(nextAction.status)) {
39
- errors.push(`next-action.json.status 无效值: ${nextAction.status}`)
40
- }
41
- if (typeof nextAction.has_blocking_questions !== 'boolean') {
42
- errors.push(`next-action.json.has_blocking_questions 不是布尔值`)
43
- }
44
- if (!['high', 'medium', 'low'].includes(nextAction.decision_level)) {
45
- warnings.push(`next-action.json.decision_level 无效值: ${nextAction.decision_level}`)
46
- }
47
-
48
- // 状态一致性
49
- if (nextAction.status === 'waiting_for_user' && nextAction.has_blocking_questions !== true) {
50
- errors.push(`next-action.json status=waiting_for_user 但 has_blocking_questions !== true`)
51
- }
52
- if (nextAction.status === 'ready_for_plan' && nextAction.has_blocking_questions === true) {
53
- errors.push(`next-action.json status=ready_for_plan 但 has_blocking_questions === true`)
54
- }
55
- } catch (e) {
56
- errors.push(`next-action.json 解析失败: ${e.message}`)
57
- }
58
- }
59
-
60
- // ── 3. design.md 覆盖度检查 ──
61
- const designFile = join(brainstormDir, 'design.md')
62
- if (existsSync(designFile)) {
63
- const designContent = readFileSync(designFile, 'utf8')
64
-
65
- // 必须覆盖的维度
66
- if (!/(?:目标|goal|objective|背景|background|问题|problem)/i.test(designContent)) {
67
- warnings.push('design.md 缺少设计目标/背景描述')
68
- }
69
- if (!/(?:范围|scope|总体方案|方案|approach)/i.test(designContent)) {
70
- warnings.push('design.md 缺少总体方案描述')
71
- }
72
-
73
- // 弱覆盖检测
74
- if (/影响模块.*TBD|文件变更.*TBD|待定/i.test(designContent)) {
75
- warnings.push('design.md 包含 TBD/待定内容')
76
- }
77
- if (!/(?:验收|acceptance|完成标准)/i.test(designContent)) {
78
- warnings.push('design.md 缺少验收标准')
79
- }
80
- }
81
-
82
- // ── 4. gaps.md 覆盖度检查 ──
83
- const gapsFile = join(brainstormDir, 'gaps.md')
84
- if (existsSync(gapsFile)) {
85
- const gapsContent = readFileSync(gapsFile, 'utf8')
86
- const hasBlocker = /BLOCKER/i.test(gapsContent)
87
- if (hasBlocker) {
88
- errors.push('gaps.md 包含 BLOCKER 级缺口,需要先解决')
89
- }
90
- // 检查是否为空
91
- const nonEmpty = gapsContent.split('\n').filter(l => l.trim() && !l.startsWith('#')).length
92
- if (nonEmpty === 0) {
93
- warnings.push('gaps.md 为空(brainstorm 可能遗漏了缺口分析)')
94
- }
95
- }
96
-
97
- // ── 5. assumptions.md 风险检查 ──
98
- const assumptionsFile = join(brainstormDir, 'assumptions.md')
99
- if (existsSync(assumptionsFile)) {
100
- const assumptionsContent = readFileSync(assumptionsFile, 'utf8')
101
- if (/假设现有数据格式不变|假设.*API.*不变|假设.*性能.*可接受/i.test(assumptionsContent)) {
102
- warnings.push('assumptions.md 包含高风险假设,需在 verify 阶段验证')
103
- }
104
- }
105
-
106
- // ── 6. decisions.md 一致性检查 ──
107
- const decisionsFile = join(brainstormDir, 'decisions.md')
108
- if (existsSync(decisionsFile)) {
109
- const decisionsContent = readFileSync(decisionsFile, 'utf8')
110
- const autoDecided = (decisionsContent.match(/AUTO_DECIDED/g) || []).length
111
- const autoDecidedWithReason = (decisionsContent.match(/AUTO_DECIDED[\s\S]*?checklist/i) || []).length
112
- if (autoDecided > 0 && autoDecidedWithReason < autoDecided) {
113
- errors.push(`${autoDecided - autoDecidedWithReason} 个 AUTO_DECIDED 决策缺少 checklist 依据`)
114
- }
115
- }
116
-
117
- // ── 判定结果 ──
118
- let level
119
- if (errors.length > 0) {
120
- level = 'FAIL'
121
- } else if (warnings.length > 0) {
122
- level = 'WARN'
123
- } else {
124
- level = 'PASS'
125
- }
126
-
127
- return { ok: level !== 'FAIL', level, errors, warnings }
128
- }
129
-
130
- /**
131
- * 执行 brainstorm postcheck(供 run.js 调用)
132
- */
133
- export async function executeBrainstormPostcheck(cwd, platformOpts, changeName) {
134
- const specBase = platformOpts?.specRoot || join(cwd, '.sillyspec')
135
- const changeDir = join(specBase, 'changes', changeName)
136
- if (!existsSync(changeDir)) {
137
- console.log('⚠️ brainstorm postcheck: 变更目录不存在,跳过')
138
- return
139
- }
140
-
141
- const result = checkBrainstormArtifacts(changeDir)
142
-
143
- if (result.level === 'PASS') {
144
- console.log('✅ brainstorm postcheck: PASS')
145
- } else if (result.level === 'WARN') {
146
- console.log('⚠️ brainstorm postcheck: WARN')
147
- for (const w of result.warnings) {
148
- console.log(` - ${w}`)
149
- }
150
- } else {
151
- console.error('❌ brainstorm postcheck: FAIL')
152
- for (const e of result.errors) {
153
- console.error(` - ${e}`)
154
- }
155
- console.error('\\n 请修复以上问题后重试,或使用 --skip-approval 跳过。')
156
- process.exit(1)
157
- }
158
- }