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.
- package/.claude/skills/sillyspec-doctor/SKILL.md +9 -0
- package/.playwright-mcp/console-2026-07-27T15-45-50-621Z.log +1 -0
- package/.playwright-mcp/page-2026-07-27T15-45-51-459Z.yml +144 -0
- package/.playwright-mcp/prompt-final.png +0 -0
- package/.playwright-mcp/prompt-view.png +0 -0
- package/CLAUDE.md +18 -0
- package/package.json +1 -1
- package/src/change-risk-profile.js +60 -12
- package/src/check-primitives.js +65 -0
- package/src/db.js +28 -8
- package/src/hooks/worktree-guard.js +9 -2
- package/src/index.js +140 -11
- package/src/machine-interface.js +13 -7
- package/src/progress/change-registry.js +217 -0
- package/src/progress/consistency-doctor.js +259 -0
- package/src/progress/shared.js +33 -0
- package/src/progress/stage-machine.js +535 -0
- package/src/progress/step-store.js +256 -0
- package/src/progress.js +75 -1191
- package/src/quicklog.js +3 -1
- package/src/run/command.js +1088 -0
- package/src/run/complete-handlers.js +874 -0
- package/src/run/complete.js +725 -0
- package/src/run/gates.js +326 -0
- package/src/run/prompt.js +619 -0
- package/src/run/quick-audit.js +107 -0
- package/src/run/scan-profile.js +238 -0
- package/src/run/shared.js +418 -0
- package/src/run/stage.js +414 -0
- package/src/run.js +23 -4379
- package/src/stage-contract-engine.js +150 -0
- package/src/stage-contract-spec.js +517 -0
- package/src/stage-contract.js +87 -150
- package/src/stage-review.js +63 -0
- package/src/stages/brainstorm.js +3 -1
- package/src/stages/execute.js +5 -2
- package/src/stages/plan-postcheck.js +38 -27
- package/src/stages/plan.js +12 -49
- package/src/stages/propose.js +2 -1
- package/src/stages/scan.js +3 -1
- package/src/workflow.js +9 -17
- package/src/worktree-apply.js +5 -2
- package/src/worktree-deps.js +114 -32
- package/src/worktree.js +23 -11
- 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
|
-
// 排除
|
|
167
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}
|